From a884776e22f3eea89f7e6d42fb9d00602ca3a14e Mon Sep 17 00:00:00 2001
From: Gboy9155 <32224410+Gboy9155@users.noreply.github.com>
Date: Sun, 29 Oct 2017 21:49:29 +0100
Subject: [PATCH] Binding youtube extractor to c#
---
MusicApp/MusicApp.csproj | 4 +
MusicApp/Resources/Portable Class/Format.cs | 167 ++++
.../Resources/Portable Class/VideoMeta.cs | 56 ++
.../Portable Class/YoutubeExtractor.cs | 725 ++++++++++++++++++
MusicApp/Resources/Portable Class/YtFile.cs | 43 ++
5 files changed, 995 insertions(+)
create mode 100644 MusicApp/Resources/Portable Class/Format.cs
create mode 100644 MusicApp/Resources/Portable Class/VideoMeta.cs
create mode 100644 MusicApp/Resources/Portable Class/YoutubeExtractor.cs
create mode 100644 MusicApp/Resources/Portable Class/YtFile.cs
diff --git a/MusicApp/MusicApp.csproj b/MusicApp/MusicApp.csproj
index 86cada9..bcaa936 100644
--- a/MusicApp/MusicApp.csproj
+++ b/MusicApp/MusicApp.csproj
@@ -157,6 +157,7 @@
+
@@ -165,7 +166,10 @@
+
+
+
diff --git a/MusicApp/Resources/Portable Class/Format.cs b/MusicApp/Resources/Portable Class/Format.cs
new file mode 100644
index 0000000..3072df0
--- /dev/null
+++ b/MusicApp/Resources/Portable Class/Format.cs
@@ -0,0 +1,167 @@
+namespace MusicApp.Resources.Portable_Class
+{
+ [System.Serializable]
+ public class Format
+ {
+ public enum VCodec
+ {
+ H263, H264, MPEG4, VP8, VP9, NONE
+ }
+
+ public enum ACodec
+ {
+ MP3, AAC, VORBIS, OPUS, NONE
+ }
+
+ private int itag;
+ private string ext;
+ public int height;
+ private int fps;
+ private VCodec vCodec;
+ private ACodec aCodec;
+ private int audioBitrate;
+ public bool isDashContainer;
+ public bool isHlsContent;
+
+ public Format(int itag, string ext, int height, VCodec vCodec, ACodec aCodec, bool isDashContainer)
+ {
+ this.itag = itag;
+ this.ext = ext;
+ this.height = height;
+ this.fps = 30;
+ this.audioBitrate = -1;
+ this.isDashContainer = isDashContainer;
+ this.isHlsContent = false;
+ }
+
+ public Format(int itag, string ext, VCodec vCodec, ACodec aCodec, int audioBitrate, bool isDashContainer)
+ {
+ this.itag = itag;
+ this.ext = ext;
+ this.height = -1;
+ this.fps = 30;
+ this.audioBitrate = audioBitrate;
+ this.isDashContainer = isDashContainer;
+ this.isHlsContent = false;
+ }
+
+ public Format(int itag, string ext, int height, VCodec vCodec, ACodec aCodec, int audioBitrate, bool isDashContainer)
+ {
+ this.itag = itag;
+ this.ext = ext;
+ this.height = height;
+ this.fps = 30;
+ this.audioBitrate = audioBitrate;
+ this.isDashContainer = isDashContainer;
+ this.isHlsContent = false;
+ }
+
+ public Format(int itag, string ext, int height, VCodec vCodec, ACodec aCodec, int audioBitrate, bool isDashContainer, bool isHlsContent)
+ {
+ this.itag = itag;
+ this.ext = ext;
+ this.height = height;
+ this.fps = 30;
+ this.audioBitrate = audioBitrate;
+ this.isDashContainer = isDashContainer;
+ this.isHlsContent = isHlsContent;
+ }
+
+ public Format(int itag, string ext, int height, VCodec vCodec, int fps, ACodec aCodec, bool isDashContainer)
+ {
+ this.itag = itag;
+ this.ext = ext;
+ this.height = height;
+ this.audioBitrate = -1;
+ this.fps = fps;
+ this.isDashContainer = isDashContainer;
+ this.isHlsContent = false;
+ }
+
+ public int GetFPS()
+ {
+ return fps;
+ }
+
+ public int GetAudioBitrate()
+ {
+ return audioBitrate;
+ }
+
+ public int GetItag()
+ {
+ return itag;
+ }
+
+ public string GetExt()
+ {
+ return ext;
+ }
+
+ public ACodec GetAudioCodec()
+ {
+ return aCodec;
+ }
+
+ public VCodec GetVideoCodec()
+ {
+ return vCodec;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null || obj.GetType() != GetType())
+ return false;
+
+ Format format = (Format)obj;
+
+ if (itag != format.itag)
+ return false;
+ if (height != format.height)
+ return false;
+ if (fps != format.fps)
+ return false;
+ if (audioBitrate != format.audioBitrate)
+ return false;
+ if (isDashContainer != format.isDashContainer)
+ return false;
+ if (isHlsContent != format.isHlsContent)
+ return false;
+ if (ext != null ? !ext.Equals(format.ext) : format.ext != null)
+ return false;
+ if (vCodec != format.vCodec)
+ return false;
+ return aCodec == format.aCodec;
+ }
+
+ public override int GetHashCode()
+ {
+ int result = itag;
+ result = 31 * result + (ext != null ? ext.GetHashCode() : 0);
+ result = 31 * result + height;
+ result = 31 * result + fps;
+ result = 31 * result + vCodec.GetHashCode();
+ result = 31 * result + aCodec.GetHashCode();
+ result = 31 * result + audioBitrate;
+ result = 31 * result + (isDashContainer ? 1 : 0);
+ result = 31 * result + (isHlsContent ? 1 : 0);
+ return result;
+ }
+
+ public override string ToString()
+ {
+ return "Format{" +
+ "itag=" + itag +
+ ", ext='" + ext + '\'' +
+ ", height=" + height +
+ ", fps=" + fps +
+ ", vCodec=" + vCodec +
+ ", aCodec=" + aCodec +
+ ", audioBitrate=" + audioBitrate +
+ ", isDashContainer=" + isDashContainer +
+ ", isHlsContent=" + isHlsContent +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/MusicApp/Resources/Portable Class/VideoMeta.cs b/MusicApp/Resources/Portable Class/VideoMeta.cs
new file mode 100644
index 0000000..ec5085d
--- /dev/null
+++ b/MusicApp/Resources/Portable Class/VideoMeta.cs
@@ -0,0 +1,56 @@
+namespace MusicApp.Resources.Portable_Class
+{
+ public class VideoMeta
+ {
+ private const string IMAGE_BASE_URL = "http://i.ytimg.com/vi/";
+
+ public string videoID;
+ public string title;
+ public string author;
+ public object channelId;
+ public object length;
+ public long viewCount;
+ public bool isLiveStream;
+
+ public VideoMeta(string videoID, string title, string author, object channelId, object length, long viewCount, bool isLiveStream)
+ {
+ this.videoID = videoID;
+ this.title = title;
+ this.author = author;
+ this.channelId = channelId;
+ this.length = length;
+ this.viewCount = viewCount;
+ this.isLiveStream = isLiveStream;
+ }
+
+ // 120 x 90
+ public string GetThumbUrl()
+ {
+ return IMAGE_BASE_URL + videoID + "/default.jpg";
+ }
+
+ // 320 x 180
+ public string GetMqImageUrl()
+ {
+ return IMAGE_BASE_URL + videoID + "/mqdefault.jpg";
+ }
+
+ // 480 x 360
+ public string GetHqImageUrl()
+ {
+ return IMAGE_BASE_URL + videoID + "/hqdefault.jpg";
+ }
+
+ // 640 x 480
+ public string GetSdImageUrl()
+ {
+ return IMAGE_BASE_URL + videoID + "/sddefault.jpg";
+ }
+
+ // Max Res
+ public string GetMaxResImageUrl()
+ {
+ return IMAGE_BASE_URL + videoID + "/maxresdefault.jpg";
+ }
+ }
+}
\ No newline at end of file
diff --git a/MusicApp/Resources/Portable Class/YoutubeExtractor.cs b/MusicApp/Resources/Portable Class/YoutubeExtractor.cs
new file mode 100644
index 0000000..978cd09
--- /dev/null
+++ b/MusicApp/Resources/Portable Class/YoutubeExtractor.cs
@@ -0,0 +1,725 @@
+using System;
+using System.Collections.Generic;
+using Android.Runtime;
+using Android.Util;
+using Java.Lang;
+using Java.Util.Regex;
+using Java.Net;
+using Java.IO;
+using Java.Util.Concurrent.Locks;
+using Android.Webkit;
+
+namespace MusicApp.Resources.Portable_Class
+{
+ public abstract class YoutubeExtractor : Android.OS.AsyncTask>, IValueCallback
+ {
+ private const int dashRetries = 5;
+ private bool parseDashManifest;
+ private bool includeWebM;
+
+ private string videoID;
+ private bool useHttps = true;
+
+ private /*volatile*/ string decipheredSignature;
+
+ private string curJsFileName;
+ private const string cacheFileName = "decipher_js_funct";
+
+ private static string decipherJsFileName;
+ private static string decipherFunctions;
+ private static string decipherFunctionName;
+
+ private static ILock Ilock = new ReentrantLock();
+ private ICondition jsExecution = Ilock.NewCondition();
+
+ private const string USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36";
+
+ private Pattern patYouTubePageLink = Pattern.Compile("(http|https)://(www\\.|m.|)youtube\\.com/watch\\?v=(.+?)( |\\z|&)");
+ private Pattern patYouTubeShortLink = Pattern.Compile("(http|https)://(www\\.|)youtu.be/(.+?)( |\\z|&)");
+
+ private Pattern patTitle = Pattern.Compile("title=(.*?)(&|\\z)");
+ private Pattern patHlsvp = Pattern.Compile("hlsvp=(.+?)(&|\\z)");
+ private Pattern patAuthor = Pattern.Compile("author=(.+?)(&|\\z)");
+ private Pattern patChannelId = Pattern.Compile("ucid=(.+?)(&|\\z)");
+ private Pattern patLength = Pattern.Compile("length_seconds=(\\d+?)(&|\\z)");
+ private Pattern patViewCount = Pattern.Compile("view_count=(\\d+?)(&|\\z)");
+
+ private Pattern patHlsItag = Pattern.Compile("/itag/(\\d+?)/");
+ private Pattern patDecryptionJsFile = Pattern.Compile("jsbin\\\\/(player-(.+?).js)");
+
+ private Pattern patDashManifest1 = Pattern.Compile("dashmpd=(.+?)(&|\\z)");
+ private Pattern patDashManifest2 = Pattern.Compile("\"dashmpd\":\"(.+?)\"");
+ private Pattern patDashManifestEncSig = Pattern.Compile("/s/([0-9A-F|\\.]{10,}?)(/|\\z)");
+
+ private Pattern patItag = Pattern.Compile("itag=([0-9]+?)(&|,)");
+ private Pattern patEncSig = Pattern.Compile("s=([0-9A-F|\\.]{10,}?)(&|,|\")");
+ private Pattern patUrl = Pattern.Compile("url=(.+?)(&|,)");
+
+ private Pattern patVariableFunction = Pattern.Compile("(\\{|;| |=)([a-zA-Z$][a-zA-Z0-9$]{0,2})\\.([a-zA-Z$][a-zA-Z0-9$]{0,2})\\(");
+ private Pattern patFunction = Pattern.Compile("(\\{|;| |=)([a-zA-Z$_][a-zA-Z0-9$]{0,2})\\(");
+ private Pattern patSignatureDecFunction = Pattern.Compile("\\(\"signature\",(.{1,3}?)\\(.{1,10}?\\)");
+
+
+ #region formats
+ private Dictionary formats = new Dictionary
+ {
+ // http://en.wikipedia.org/wiki/YouTube#Quality_and_formats
+
+ // Video and Audio
+ { 17, new Format(17, "3gp", 144, Format.VCodec.MPEG4, Format.ACodec.AAC, 24, false) },
+ {36, new Format(36, "3gp", 240, Format.VCodec.MPEG4, Format.ACodec.AAC, 32, false)},
+ {5, new Format(5, "flv", 240, Format.VCodec.H263, Format.ACodec.MP3, 64, false)},
+ {43, new Format(43, "webm", 360, Format.VCodec.VP8, Format.ACodec.VORBIS, 128, false)},
+ {18, new Format(18, "mp4", 360, Format.VCodec.H264, Format.ACodec.AAC, 96, false)},
+ {22, new Format(22, "mp4", 720, Format.VCodec.H264, Format.ACodec.AAC, 192, false)},
+
+ // Dash Video
+ {160, new Format(160, "mp4", 144, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {133, new Format(133, "mp4", 240, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {134, new Format(134, "mp4", 360, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {135, new Format(135, "mp4", 480, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {136, new Format(136, "mp4", 720, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {137, new Format(137, "mp4", 1080, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {264, new Format(264, "mp4", 1440, Format.VCodec.H264, Format.ACodec.NONE, true)},
+ {266, new Format(266, "mp4", 2160, Format.VCodec.H264, Format.ACodec.NONE, true)},
+
+ {298, new Format(298, "mp4", 720, Format.VCodec.H264, 60, Format.ACodec.NONE, true)},
+ {299, new Format(299, "mp4", 1080, Format.VCodec.H264, 60, Format.ACodec.NONE, true)},
+
+ // Dash Audio
+ {140, new Format(140, "m4a", Format.VCodec.NONE, Format.ACodec.AAC, 128, true)},
+ {141, new Format(141, "m4a", Format.VCodec.NONE, Format.ACodec.AAC, 256, true)},
+
+ // WEBM Dash Video
+ {278, new Format(278, "webm", 144, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {242, new Format(242, "webm", 240, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {243, new Format(243, "webm", 360, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {244, new Format(244, "webm", 480, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {247, new Format(247, "webm", 720, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {248, new Format(248, "webm", 1080, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {271, new Format(271, "webm", 1440, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+ {313, new Format(313, "webm", 2160, Format.VCodec.VP9, Format.ACodec.NONE, true)},
+
+ {302, new Format(302, "webm", 720, Format.VCodec.VP9, 60, Format.ACodec.NONE, true)},
+ {308, new Format(308, "webm", 1440, Format.VCodec.VP9, 60, Format.ACodec.NONE, true)},
+ {303, new Format(303, "webm", 1080, Format.VCodec.VP9, 60, Format.ACodec.NONE, true)},
+ {315, new Format(315, "webm", 2160, Format.VCodec.VP9, 60, Format.ACodec.NONE, true)},
+
+ // WEBM Dash Audio
+ {171, new Format(171, "webm", Format.VCodec.NONE, Format.ACodec.VORBIS, 128, true)},
+
+ {249, new Format(249, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 48, true)},
+ {250, new Format(250, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 64, true)},
+ {251, new Format(251, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 160, true)},
+
+ // HLS Live Stream
+ {91, new Format(91, "mp4", 144 , Format.VCodec.H264, Format.ACodec.AAC, 48, false, true)},
+ {92, new Format(92, "mp4", 240 , Format.VCodec.H264, Format.ACodec.AAC, 48, false, true)},
+ {93, new Format(93, "mp4", 360 , Format.VCodec.H264, Format.ACodec.AAC, 128, false, true)},
+ {94, new Format(94, "mp4", 480 , Format.VCodec.H264, Format.ACodec.AAC, 128, false, true)},
+ {95, new Format(95, "mp4", 720 , Format.VCodec.H264, Format.ACodec.AAC, 256, false, true)},
+ {96, new Format(96, "mp4", 1080 , Format.VCodec.H264, Format.ACodec.AAC, 256, false, true)},
+ };
+#endregion
+
+
+
+ public YoutubeExtractor(IntPtr doNotUse, JniHandleOwnership transfer) : base(doNotUse, transfer)
+ {
+
+ }
+
+ public void Extract(string youtubeURL, bool parseDashManifest, bool includeWebM)
+ {
+ this.parseDashManifest = parseDashManifest;
+ this.includeWebM = includeWebM;
+ this.Execute(youtubeURL);
+ }
+
+ protected override void OnPreExecute()
+ {
+ base.OnPreExecute();
+ }
+
+ protected override void OnPostExecute(SparseArray ytFiles)
+ {
+ OnExtractionComplete(ytFiles);
+ }
+
+ protected abstract void OnExtractionComplete(SparseArray ytFiles);
+
+ protected override Java.Lang.Object DoInBackground(params Java.Lang.Object[] native_parms)
+ {
+ videoID = null;
+ string ytUrl = native_parms[0].ToString();
+ if (ytUrl == null)
+ return null;
+
+ Matcher matcher = patYouTubePageLink.Matcher(ytUrl);
+ if (matcher.Find())
+ videoID = matcher.Group(3);
+ else
+ {
+ matcher = patYouTubeShortLink.Matcher(ytUrl);
+ if (matcher.Find())
+ videoID = matcher.Group(3);
+ else if (new Java.Lang.String(videoID).Matches("\\p{Graph}+?"))
+ videoID = ytUrl;
+ }
+ if (videoID != null)
+ {
+ try
+ {
+ return GetStreamUrls();
+ }
+ catch (Java.Lang.Exception e)
+ {
+ e.PrintStackTrace();
+ }
+ }
+ else
+ System.Console.WriteLine("Youtube link not supported");
+ return null;
+ }
+
+ private SparseArray GetStreamUrls()
+ {
+ string ytInfoUrl = (useHttps) ? "https://" : "http://";
+ ytInfoUrl += "www.youtube.com/get_video_info?video_id=" + videoID + "&eurl=" + URLEncoder.Encode("https://youtube.googleapis.com/v/" + videoID, "UTF-8");
+
+ //string dashMpdUrl = null;
+ string streamMap = null;
+ BufferedReader reader = null;
+ URL url = new URL(ytInfoUrl);
+ HttpURLConnection urlConnection = (HttpURLConnection)url.OpenConnection();
+ urlConnection.SetRequestProperty("User-Agent", USER_AGENT);
+
+ try
+ {
+ reader = new BufferedReader(new InputStreamReader(urlConnection.InputStream));
+ streamMap = reader.ReadLine();
+ }
+ finally
+ {
+ if (reader != null)
+ reader.Close();
+ urlConnection.Disconnect();
+ }
+
+ VideoMeta videoMeta = ParseVideoMeta(streamMap);
+ if (videoMeta.isLiveStream)
+ {
+ return GetLiveStreamUrls(streamMap);
+ }
+
+ SparseArray encSignatures = new SparseArray();
+ string dashMpdUrl = null;
+
+ if (streamMap == null || !streamMap.Contains("use_cipher_signature=False"))
+ encSignatures = DecipherJsFile(streamMap);
+ else
+ {
+ if (parseDashManifest)
+ {
+ Matcher matcher = patDashManifest1.Matcher(streamMap);
+ if (matcher.Find())
+ {
+ dashMpdUrl = URLDecoder.Decode(matcher.Group(1), "UTF-8");
+ }
+ }
+ streamMap = URLDecoder.Decode(streamMap, "UTF-8");
+ }
+ Java.Lang.String streamMapJL = new Java.Lang.String(streamMap);
+ string[] streams = streamMapJL.Split(",|url_encoded_fmt_stream_map|&adaptive_fmts=");
+ SparseArray ytFiles = new SparseArray();
+
+ foreach(string foo in streams)
+ {
+ string encStream = foo + ",";
+ if (!encStream.Contains("itag%3D"))
+ continue;
+
+ string stream = URLDecoder.Decode(encStream, "UTF-8");
+
+ Matcher matcher = patItag.Matcher(stream);
+ int itag;
+ if (matcher.Find())
+ {
+ itag = int.Parse(matcher.Group(1));
+ if (formats[itag] == null)
+ continue;
+ else if (!includeWebM && formats[itag].GetExt().Equals("webm"))
+ continue;
+ }
+ else
+ continue;
+
+ if (curJsFileName != null)
+ {
+ matcher = patEncSig.Matcher(stream);
+ if (matcher.Find())
+ {
+ encSignatures.Append(itag, matcher.Group(1));
+ }
+ }
+ matcher = patUrl.Matcher(encStream);
+ string uri = null;
+ if (matcher.Find())
+ uri = matcher.Group(1);
+
+ if(uri != null)
+ {
+ Format format = formats[itag];
+ string finalUrl = URLDecoder.Decode(uri, "UTF-8");
+ YtFile newVideo = new YtFile(format, finalUrl);
+ ytFiles.Put(itag, newVideo);
+ }
+ }
+ if(encSignatures != null)
+ {
+ decipheredSignature = null;
+ if (DecipherSignature(encSignatures))
+ {
+ Ilock.Lock();
+ try
+ {
+ jsExecution.Await(7, Java.Util.Concurrent.TimeUnit.Seconds);
+ }
+ finally
+ {
+ Ilock.Unlock();
+ }
+ }
+ if (decipheredSignature == null)
+ return null;
+ else
+ {
+ string[] signatures = new Java.Lang.String(decipheredSignature).Split("\n");
+ for (int i = 0; i < encSignatures.Size() && i < signatures.Length; i++)
+ {
+ int key = encSignatures.KeyAt(i);
+ if (key == 0)
+ dashMpdUrl = dashMpdUrl.Replace("/s/" + encSignatures.Get(key), "/signature/" + signatures[i]);
+ else
+ {
+ string uri = ytFiles.Get(key).url;
+ uri += "&signature=" + signatures[i];
+ YtFile newFile = new YtFile(formats[key], uri);
+ ytFiles.Put(key, newFile);
+ }
+ }
+ }
+ }
+ if(parseDashManifest && dashMpdUrl != null)
+ {
+ for (int i = 0; i < dashRetries; i++)
+ {
+ try
+ {
+ ParseDashManifest(dashMpdUrl, ytFiles);
+ break;
+ }
+ catch(IOException)
+ {
+ Thread.Sleep(5);
+ }
+ }
+ }
+ if (ytFiles.Size() == 0)
+ return null;
+ return ytFiles;
+ }
+
+ private bool DecipherSignature(SparseArray encSignatures)
+ {
+ if(decipherFunctionName == null || decipherFunctions == null)
+ {
+ string decipherFunctUrl = "https://s.ytimg.com/yts/jsbin/" + decipherJsFileName;
+ URL url = new URL(decipherFunctUrl);
+ HttpURLConnection urlConnection = (HttpURLConnection)url.OpenConnection();
+ urlConnection.SetRequestProperty("User-Agent", USER_AGENT);
+ BufferedReader reader = null;
+ string javascriptFile = null;
+ try
+ {
+ reader = new BufferedReader(new InputStreamReader(urlConnection.InputStream));
+ StringBuilder sb = new StringBuilder("");
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ sb.Append(line);
+ sb.Append(" ");
+ }
+ javascriptFile = sb.ToString();
+ }
+ finally
+ {
+ if (reader != null)
+ reader.Close();
+ urlConnection.Disconnect();
+ }
+
+ Matcher matcher = patSignatureDecFunction.Matcher(javascriptFile);
+ if (matcher.Find())
+ {
+ decipherFunctionName = matcher.Group(1);
+ Pattern patMainVariable = Pattern.Compile("(var |\\s|,|;)" + decipherFunctionName.Replace("$", "\\$") + "(=function\\((.{1,3})\\)\\{)");
+ string mainDecipherFunct;
+
+ matcher = patMainVariable.Matcher(javascriptFile);
+ if (matcher.Find())
+ mainDecipherFunct = "var " + decipherFunctionName + matcher.Group(2);
+ else
+ {
+ Pattern patMainFunction = Pattern.Compile("function " + decipherFunctionName.Replace("$", "\\$") + "(\\((.{1,3})\\)\\{)");
+ matcher = patMainFunction.Matcher(javascriptFile);
+ if (!matcher.Find())
+ return false;
+ mainDecipherFunct = "function " + decipherFunctionName + matcher.Group(2);
+ }
+
+ int startIndex = matcher.End();
+ char[] javascriptChars = javascriptFile.ToCharArray();
+ for (int braces = 1, i = 0; i < javascriptFile.Length; i++)
+ {
+ if (braces == 0 && startIndex + 5 < i)
+ {
+ mainDecipherFunct += javascriptFile.Substring(startIndex, i) + ";";
+ break;
+ }
+ if (javascriptChars[i] == '{')
+ braces++;
+ else if (javascriptChars[i] == '}')
+ braces--;
+ }
+ decipherFunctions = mainDecipherFunct;
+ matcher = patVariableFunction.Matcher(mainDecipherFunct);
+ while (matcher.Find())
+ {
+ string variableDef = "var " + matcher.Group(2) + "={";
+ if (decipherFunctions.Contains(variableDef))
+ continue;
+
+ startIndex = javascriptFile.IndexOf(variableDef) + variableDef.Length;
+ javascriptChars = javascriptFile.ToCharArray();
+ for (int braces = 1, i = startIndex; i < javascriptFile.Length; i++)
+ {
+ if (braces == 0)
+ {
+ decipherFunctions += variableDef + javascriptFile.Substring(startIndex, i) + ";";
+ break;
+ }
+ if (javascriptChars[i] == '{')
+ braces++;
+ else if (javascriptChars[i] == '}')
+ braces--;
+ }
+ }
+
+ matcher = patFunction.Matcher(mainDecipherFunct);
+ while (matcher.Find())
+ {
+ string functionDef = "function " + matcher.Group(2) + "(";
+ if (decipherFunctions.Contains(functionDef))
+ continue;
+
+ startIndex = javascriptFile.IndexOf(functionDef) + functionDef.Length;
+ javascriptChars = javascriptFile.ToCharArray();
+ for (int braces = 0, i = startIndex; i < javascriptFile.Length; i++)
+ {
+ if (braces == 0 && startIndex + 5 < i)
+ {
+ decipherFunctions += functionDef + javascriptFile.Substring(startIndex, i) + ";";
+ break;
+ }
+ if (javascriptChars[i] == '{')
+ braces++;
+ else if (javascriptChars[i] == '}')
+ braces--;
+ }
+ }
+
+ DecipherViaWebView(encSignatures);
+ WriteDeciperFunctToCache();
+ }
+ else
+ return false;
+ }
+ else
+ DecipherViaWebView(encSignatures);
+ return true;
+ }
+
+ private void DecipherViaWebView(SparseArray encSignatures)
+ {
+ StringBuilder stringBuilder = new StringBuilder(decipherFunctions + " function decipher(");
+ stringBuilder.Append("){return ");
+ for (int i = 0; i < encSignatures.Size(); i++)
+ {
+ int key = encSignatures.KeyAt(i);
+ if (i < encSignatures.Size() - 1)
+ stringBuilder.Append(decipherFunctionName).Append("('").Append(encSignatures.Get(key)).Append("')+\"\\n\"+");
+ else
+ stringBuilder.Append(decipherFunctionName).Append("('").Append(encSignatures.Get(key)).Append("')");
+ }
+ stringBuilder.Append("};decipher();");
+
+ Android.OS.Handler handler = new Android.OS.Handler((sender) =>
+ {
+ WebView webView = new WebView(Android.App.Application.Context);
+ webView.EvaluateJavascript(stringBuilder.ToString(), this);
+ });
+ }
+
+ public void OnReceiveValue(Java.Lang.Object value)
+ {
+ Ilock.Lock();
+ try
+ {
+ decipheredSignature = value.ToString();
+ }
+ finally
+ {
+ Ilock.Unlock();
+ }
+ }
+
+ private void WriteDeciperFunctToCache()
+ {
+ File cacheFile = new File(Android.App.Application.Context.CacheDir.AbsolutePath + "/" + cacheFileName);
+ BufferedWriter writer = null;
+ try
+ {
+ writer = new BufferedWriter(new FileWriter(cacheFile));
+ writer.Write(decipherJsFileName + "\n");
+ writer.Write(decipherFunctionName + "\n");
+ writer.Write(decipherFunctions);
+ }
+ catch (Java.Lang.Exception e)
+ {
+ e.PrintStackTrace();
+ }
+ finally
+ {
+ if (writer != null)
+ writer.Close();
+ }
+ }
+
+ private void ParseDashManifest(string dashMpdUrl, SparseArray ytFiles)
+ {
+ Pattern patBaseUrl = Pattern.Compile("(.+?)");
+ URL url = new URL(dashMpdUrl);
+ HttpURLConnection urlConnection = (HttpURLConnection)url.OpenConnection();
+ urlConnection.SetRequestProperty("User-Agent", USER_AGENT);
+ BufferedReader reader = null;
+ string dashManifest;
+ try
+ {
+ reader = new BufferedReader(new InputStreamReader(urlConnection.InputStream));
+ reader.ReadLine();
+ dashManifest = reader.ReadLine();
+ }
+ finally
+ {
+ if (reader != null)
+ reader.Close();
+ urlConnection.Disconnect();
+ }
+ if (dashManifest == null)
+ return;
+
+ Matcher matcher = patBaseUrl.Matcher(dashManifest);
+ while (matcher.Find())
+ {
+ string foo = matcher.Group(1);
+ Matcher matcherBis = patItag.Matcher(foo);
+ int itag;
+ if (matcherBis.Find())
+ {
+ itag = int.Parse(matcherBis.Group(1));
+ if (formats[itag] != null)
+ continue;
+ if (!includeWebM && formats[itag].GetExt().Equals("webm"))
+ continue;
+ }
+ else
+ continue;
+
+ foo = foo.Replace("&", "&").Replace(",", "%2C").Replace("mime=audio/", "mime=audio%2F").Replace("mime=video/", "mime=video%2F");
+ YtFile newFile = new YtFile(formats[itag], foo);
+ ytFiles.Append(itag, newFile);
+ }
+ }
+
+ private void ReadDecipherFunctFromCache()
+ {
+ File cacheFile = new File(Android.App.Application.Context.CacheDir.AbsolutePath + "/" + cacheFileName);
+ if(cacheFile.Exists() && JavaSystem.CurrentTimeMillis() - cacheFile.LastModified() < 1209600000)
+ {
+ BufferedReader reader = null;
+ try
+ {
+ reader = new BufferedReader(new FileReader(cacheFile));
+ decipherJsFileName = reader.ReadLine();
+ decipherFunctionName = reader.ReadLine();
+ decipherFunctions = reader.ReadLine();
+ }
+ catch(Java.Lang.Exception ex)
+ {
+ ex.PrintStackTrace();
+ }
+ finally
+ {
+ if(reader != null)
+ reader.Close();
+ }
+ }
+ }
+
+ private SparseArray DecipherJsFile(string streamMap)
+ {
+ if (decipherJsFileName == null || decipherFunctions == null || decipherFunctionName == null)
+ ReadDecipherFunctFromCache();
+
+ URL url = new URL("https://youtube.com/watch?v=" + videoID);
+ HttpURLConnection urlConnection = (HttpURLConnection)url.OpenConnection();
+ urlConnection.SetRequestProperty("User-Agent", USER_AGENT);
+
+ BufferedReader reader = null;
+ try
+ {
+ reader = new BufferedReader(new InputStreamReader(urlConnection.InputStream));
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ if (line.Contains("url_encoded_fmt_stream_map"))
+ {
+ streamMap = line.Replace("\\u0026", "&");
+ break;
+ }
+ }
+ }
+ finally
+ {
+ if (reader != null)
+ reader.Close();
+ urlConnection.Disconnect();
+ }
+ SparseArray encSignatures = new SparseArray();
+
+ Matcher matcher = patDecryptionJsFile.Matcher(streamMap);
+ if (matcher.Find())
+ {
+ curJsFileName = matcher.Group(1).Replace("\\/", "/");
+ if (decipherJsFileName == null || !decipherJsFileName.Equals(curJsFileName))
+ {
+ decipherFunctions = null;
+ decipherFunctionName = null;
+ }
+ decipherJsFileName = curJsFileName;
+ }
+
+ if (parseDashManifest)
+ {
+ matcher = patDashManifest2.Matcher(streamMap);
+ if (matcher.Find())
+ {
+ string dashMpdUrl = matcher.Group(1).Replace("\\/", "/");
+ matcher = patDashManifestEncSig.Matcher(dashMpdUrl);
+ if (matcher.Find())
+ {
+ encSignatures.Append(0, matcher.Group(1));
+ }
+ else
+ {
+ dashMpdUrl = null;
+ }
+ }
+ }
+ return encSignatures;
+ }
+
+ private SparseArray GetLiveStreamUrls(string streamMap)
+ {
+ Matcher matcher = patHlsvp.Matcher(streamMap);
+ if (matcher.Find())
+ {
+ string hlsvp = URLDecoder.Decode(matcher.Group(1), "UTF-8");
+ SparseArray ytFiles = new SparseArray();
+
+ URL url = new URL(hlsvp);
+ HttpURLConnection urlConnection = (HttpURLConnection)url.OpenConnection();
+ urlConnection.SetRequestProperty("User-Agent", USER_AGENT);
+
+ BufferedReader reader = null;
+ try
+ {
+ reader = new BufferedReader(new InputStreamReader(urlConnection.InputStream));
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ if (line.StartsWith("https://") || line.StartsWith("http://"))
+ {
+ matcher = patHlsItag.Matcher(line);
+ if (matcher.Find())
+ {
+ int itag = int.Parse(matcher.Group(1));
+ YtFile newFile = new YtFile(formats[itag], line);
+ ytFiles.Put(itag, newFile);
+ }
+ }
+ }
+ }
+ finally
+ {
+ if (reader != null)
+ reader.Close();
+ urlConnection.Disconnect();
+ }
+
+ if (ytFiles.Size() == 0)
+ return null;
+ return ytFiles;
+ }
+ return null;
+ }
+
+ private VideoMeta ParseVideoMeta(string videoInfo)
+ {
+ bool isLiveStream = false;
+ string title = null;
+ string author = null;
+ string channelID = null;
+ long viewCount = 0;
+ long length = 0;
+
+ Matcher matcher = patTitle.Matcher(videoInfo);
+ if (matcher.Find())
+ title = URLDecoder.Decode(matcher.Group(1), "UTF-8");
+
+ matcher = patHlsvp.Matcher(videoInfo);
+ if (matcher.Find())
+ isLiveStream = true;
+
+ matcher = patAuthor.Matcher(videoInfo);
+ if (matcher.Find())
+ author = URLDecoder.Decode(matcher.Group(1), "UTF-8");
+
+ matcher = patChannelId.Matcher(videoInfo);
+ if (matcher.Find())
+ channelID = matcher.Group(1);
+
+ matcher = patLength.Matcher(videoInfo);
+ if (matcher.Find())
+ length = Long.ParseLong(matcher.Group(1));
+
+ matcher = patViewCount.Matcher(videoInfo);
+ if (matcher.Find())
+ viewCount = Long.ParseLong(matcher.Group(1));
+
+ return new VideoMeta(videoID, title, author, channelID, length, viewCount, isLiveStream);
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/MusicApp/Resources/Portable Class/YtFile.cs b/MusicApp/Resources/Portable Class/YtFile.cs
new file mode 100644
index 0000000..d261bca
--- /dev/null
+++ b/MusicApp/Resources/Portable Class/YtFile.cs
@@ -0,0 +1,43 @@
+namespace MusicApp.Resources.Portable_Class
+{
+ public class YtFile
+ {
+ public Format format;
+ public string url;
+
+ public YtFile(Format format, string url)
+ {
+ this.format = format;
+ this.url = url;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null || GetType() != obj.GetType())
+ return false;
+
+ YtFile ytFile = (YtFile)obj;
+
+ if (format != null ? !format.Equals(ytFile.format) : ytFile.format != null)
+ return false;
+ return url != null ? url.Equals(ytFile.url) : ytFile.url == null;
+ }
+
+ public override int GetHashCode()
+ {
+ int result = format != null ? format.GetHashCode() : 0;
+ result = 31 * result + (url != null ? url.GetHashCode() : 0);
+ return result;
+ }
+
+ public override string ToString()
+ {
+ return "YtFile{" +
+ "format=" + format +
+ ", url='" + url + '\'' +
+ '}';
+ }
+ }
+}
\ No newline at end of file