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