예제 #1
0
        private R <PlayResource, LocalStr> YoutubeDlWrapped(string link)
        {
            Log.Debug("Falling back to youtube-dl!");

            var result = YoutubeDlHelper.GetSingleVideo(link);

            if (!result.Ok)
            {
                return(result.Error);
            }

            var response = result.Value;
            var title    = response.title ?? $"Soundcloud-{link}";
            var format   = YoutubeDlHelper.FilterBest(response.formats);
            var url      = format?.url;

            if (string.IsNullOrEmpty(url))
            {
                return(new LocalStr(strings.error_ytdl_empty_response));
            }

            Log.Debug("youtube-dl succeeded!");

            return(new PlayResource(url, new AudioResource(link, StringNormalize.Normalize(title), ResolverFor)));
        }
예제 #2
0
        public R <PlayResource, LocalStr> GetResourceById(ResolveContext _, AudioResource resource)
        {
            var result = DownloadEmbeddedSite(resource.ResourceId);

            if (!result.Ok)
            {
                return(result.Error);
            }
            var webSite = result.Value;

            if (resource.ResourceTitle == null)
            {
                var nameMatch = TrackNameRegex.Match(webSite);
                resource = resource.WithTitle(nameMatch.Success
                                        ? StringNormalize.Normalize(nameMatch.Groups[1].Value)
                                        : $"Bandcamp (id: {resource.ResourceId})");
            }

            var match = TrackLinkRegex.Match(webSite);

            if (!match.Success)
            {
                return(new LocalStr(strings.error_media_internal_missing + " (TrackLinkRegex)"));
            }

            return(new BandcampPlayResource(match.Groups[1].Value, resource, GetTrackArtId(webSite)));
        }
예제 #3
0
        public R <PlayResource, LocalStr> GetResourceById(ResolveContext ctx, AudioResource resource)
        {
            var result = ValidateFromString(ctx.Config, resource.ResourceId);

            if (!result)
            {
                return(result.Error);
            }

            var resData = result.Value;

            if (resData.IsIcyStream)
            {
                if (resource.ResourceTitle == null)
                {
                    resource = resource.WithTitle(StringNormalize.Normalize(resData.Title));
                }
                return(new MediaPlayResource(resData.FullUri, resource, null, true));
            }

            if (resource.ResourceTitle == null)
            {
                resource = resource.WithTitle(!string.IsNullOrWhiteSpace(resData.Title) ? StringNormalize.Normalize(resData.Title) : resource.ResourceId);
            }

            return(new MediaPlayResource(resData.FullUri, resource, resData.Image, false));
        }
예제 #4
0
        private static R <PlayResource, LocalStr> ParseLiveData(AudioResource resource, JsonPlayerResponse parsed)
        {
            var webListResponse = WebWrapper.GetResponse(new Uri(parsed.streamingData.hlsManifestUrl), response =>
            {
                return(AudioTags.M3uReader.TryGetData(response.GetResponseStream()).OkOr(null));
            });

            if (webListResponse.Ok)
            {
                const string AacHe = "mp4a.40.5";
                const string AacLc = "mp4a.40.2";

                var webList    = webListResponse.Value;
                var streamPref = from item in webList
                                 let codecs = item.StreamMeta != null?StreamCodecMatch.Match(item.StreamMeta).Groups[1].Value : ""
                                              let codecPref = codecs.Contains(AacLc) ? 0
                                                                                                                                                                         : codecs.Contains(AacHe) ? 1
                                                                                                                                                                         : 2
                                                              let bitrate = item.StreamMeta != null?int.Parse(StreamBitrateMatch.Match(item.StreamMeta).Groups[1].Value) : int.MaxValue
                                                                            orderby codecPref, bitrate ascending
                select item;

                var streamSelect = streamPref.FirstOrDefault();
                if (streamSelect != null)
                {
                    if (resource.ResourceTitle == null)
                    {
                        resource = resource.WithTitle(StringNormalize.Normalize(parsed.videoDetails.title));
                    }
                    return(new PlayResource(streamSelect.TrackUrl, resource));
                }
            }
            return(new LocalStr(strings.error_media_no_stream_extracted));
        }
예제 #5
0
        public R <PlayResource, LocalStr> GetResourceById(ResolveContext _, AudioResource resource) => GetResourceById(resource.ResourceId, resource.ResourceTitle, true);        // TODO maybe

        private R <PlayResource, LocalStr> GetResourceById(string id, string title, bool allowNullName)
        {
            if (SoundcloudLink.IsMatch(id))
            {
                return(GetResource(null, id));
            }

//			if (id is null)
//			{
//				if (!allowNullName) return new LocalStr(strings.error_media_internal_missing + " (title)");
//				string link = RestoreLink(null, resource);
//				if (link is null) return new LocalStr(strings.error_media_internal_missing + " (link)");
//				return GetResource(null, link);
//			}

            var result = WebWrapper.DownloadString(new Uri($"https://api.soundcloud.com/tracks/{id}/streams?client_id={SoundcloudClientId}"));

            if (!result.Ok)
            {
                return(result.Error);
            }

            var json = JsonConvert.DeserializeObject <Dictionary <string, string> >(result.Value);

            return(new PlayResource(json["http_mp3_128_url"], new AudioResource(id, StringNormalize.Normalize(title), ResolverFor)));
        }
예제 #6
0
        public R <PlayResource, LocalStr> GetResource(ResolveContext _, string url)
        {
            var match = BandcampUrlRegex.Match(url);

            if (!match.Success)
            {
                return(new LocalStr(strings.error_media_invalid_uri));
            }

            var artistName = match.Groups[1].Value;
            var trackName  = match.Groups[2].Value;

            var uri = new Uri($"https://{artistName}.bandcamp.com/track/{trackName}");

            if (!WebWrapper.DownloadString(out string webSite, uri))
            {
                return(new LocalStr(strings.error_net_no_connection));
            }

            match = TrackMainJsonRegex.Match(webSite);
            if (!match.Success)
            {
                return(new LocalStr(strings.error_media_internal_missing + " (TrackMainJsonRegex)"));
            }

            JToken jobj;

            try { jobj = JToken.Parse(match.Groups[1].Value); }
            catch (JsonReaderException) { return(new LocalStr(strings.error_media_internal_missing + " (TrackMainJsonRegex.JToken)")); }

            if (!(jobj is JArray jarr) || jarr.Count == 0)
            {
                return(new LocalStr(strings.error_media_no_stream_extracted));
            }

            var firstTrack = jarr[0];

            if (!firstTrack.TryCast <string>("track_id", out var id) ||
                !firstTrack.TryCast <string>("title", out var title) ||
                firstTrack["file"] == null ||
                !firstTrack["file"].TryCast <string>("mp3-128", out var trackObj))
            {
                return(new LocalStr(strings.error_media_no_stream_extracted));
            }

            return(new BandcampPlayResource(trackObj,
                                            new AudioResource(id, StringNormalize.Normalize(title), ResolverFor, new Dictionary <string, string> {
                { AddArtist, artistName }, { AddTrack, trackName }
            }),
                                            GetTrackArtId(webSite)));
        }
예제 #7
0
        public R <IList <AudioResource>, LocalStr> SearchYoutubeDl(string keyword)
        {
            var result = YoutubeDlHelper.GetSearch(keyword);

            if (!result.Ok)
            {
                return(result.Error);
            }
            var search = result.Value;

            return(search.entries.Select(entry =>
                                         new AudioResource(
                                             entry.id,
                                             StringNormalize.Normalize(entry.title),
                                             ResolverFor
                                             )).ToArray());
        }
예제 #8
0
        private AudioResource CheckAndGet(JsonTrackInfo track)
        {
            if (track == null || track.id == 0 || track.title == null ||
                track.permalink == null || track.user?.permalink == null)
            {
                Log.Debug("Parts of track response are empty: {@json}", track);
                return(null);
            }

            return(new AudioResource(
                       track.id.ToString(CultureInfo.InvariantCulture),
                       StringNormalize.Normalize(track.title),
                       ResolverFor,
                       new Dictionary <string, string> {
                { AddArtist, track.user.permalink }, { AddTrack, track.permalink }
            }));
        }
예제 #9
0
        private R <Playlist, LocalStr> GetPlaylistYoutubeApi(string id, Uid owner)
        {
            var plist = new Playlist(id, owner);

            string nextToken = null;

            do
            {
                var queryString =
                    new Uri("https://www.googleapis.com/youtube/v3/playlistItems"
                            + "?part=contentDetails,snippet"
                            + "&fields=" + Uri.EscapeDataString("items(contentDetails/videoId,snippet/title),nextPageToken")
                            + "&maxResults=50"
                            + "&playlistId=" + id
                            + (nextToken != null ? "&pageToken=" + nextToken : string.Empty)
                            + "&key=" + YoutubeProjectId);

                if (!WebWrapper.DownloadString(out string response, queryString))
                {
                    return(new LocalStr(strings.error_net_unknown));
                }
                var parsed     = JsonConvert.DeserializeObject <JsonVideoListResponse>(response);
                var videoItems = parsed.items;
                if (!plist.AddRange(
                        videoItems.Select(item =>
                                          new PlaylistItem(
                                              new AudioResource(
                                                  item.contentDetails.videoId,
                                                  StringNormalize.Normalize(item.snippet.title),
                                                  ResolverFor
                                                  )
                                              )
                                          )
                        ))
                {
                    break;
                }

                nextToken = parsed.nextPageToken;
            } while (nextToken != null);

            return(plist);
        }
예제 #10
0
        public R <PlayResource, LocalStr> GetResourceById(ResolveContext _, AudioResource resource) => GetResourceById(resource.UniqueId, resource.ResourceTitle, true);        // TODO maybe

        private R <PlayResource, LocalStr> GetResourceById(string id, string title, bool allowNullName)
        {
            if (SoundcloudLink.IsMatch(id))
            {
                return(GetResource(null, id));
            }

//			if (id is null)
//			{
//				if (!allowNullName) return new LocalStr(strings.error_media_internal_missing + " (title)");
//				string link = RestoreLink(null, resource);
//				if (link is null) return new LocalStr(strings.error_media_internal_missing + " (link)");
//				return GetResource(null, link);
//			}

            string finalRequest = $"https://api.soundcloud.com/tracks/{id}/stream?client_id={SoundcloudClientId}";

            return(new PlayResource(finalRequest, new AudioResource(id, StringNormalize.Normalize(title), ResolverFor)));
        }
예제 #11
0
        private R <Playlist, LocalStr> GetPlaylistYoutubeDl(string id, Uid owner)
        {
            var result = YoutubeDlHelper.GetPlaylist(id);

            if (!result.Ok)
            {
                return(result.Error);
            }

            var plistData = result.Value;
            var plist     = new Playlist(owner);

            plist.AddRange(plistData.entries.Select(entry =>
                                                    new AudioResource(
                                                        entry.id,
                                                        StringNormalize.Normalize(entry.title),
                                                        ResolverFor
                                                        )
                                                    ));

            return(plist);
        }
예제 #12
0
        public R <IList <AudioResource>, LocalStr> SearchYoutubeApi(string keyword)
        {
            const int maxResults = 10;

            if (!WebWrapper.DownloadString(out string response,
                                           new Uri("https://www.googleapis.com/youtube/v3/search"
                                                   + "?part=snippet"
                                                   + "&fields=" + Uri.EscapeDataString("items(id/videoId,snippet(channelTitle,title))")
                                                   + "&type=video"
                                                   + "&safeSearch=none"
                                                   + "&q=" + Uri.EscapeDataString(keyword)
                                                   + "&maxResults=" + maxResults
                                                   + "&key=" + YoutubeProjectId)))
            {
                return(new LocalStr(strings.error_net_no_connection));
            }

            var parsed = JsonConvert.DeserializeObject <JsonSearchListResponse>(response);

            return(parsed.items.Select(x => new AudioResource(
                                           x.id.videoId,
                                           StringNormalize.Normalize(x.snippet.title),
                                           ResolverFor)).ToArray());
        }
예제 #13
0
        private static List <AudioResource> ReadListItems(StreamReader reader)
        {
            var items = new List <AudioResource>();

            string line;

            while ((line = reader.ReadLine()) != null)
            {
                var kvp = line.Split(new[] { ':' }, 2);
                if (kvp.Length < 2)
                {
                    continue;
                }

                string key   = kvp[0];
                string value = kvp[1];

                switch (key)
                {
                // Legacy entry
                case "rs":
                {
                    var rskvp = value.Split(new[] { ':' }, 2);
                    if (kvp.Length < 2)
                    {
                        Log.Warn("Erroneus playlist split count: {0}", line);
                        continue;
                    }
                    string optOwner = rskvp[0];
                    string content  = rskvp[1];

                    var rsSplit = content.Split(new[] { ',' }, 3);
                    if (rsSplit.Length < 3)
                    {
                        goto default;
                    }
                    if (!string.IsNullOrWhiteSpace(rsSplit[0]))
                    {
                        var resource = new AudioResource(Uri.UnescapeDataString(rsSplit[1]),
                                                         StringNormalize.Normalize(Uri.UnescapeDataString(rsSplit[2])), rsSplit[0]);
                        items.Add(resource);
                    }
                    else
                    {
                        goto default;
                    }

                    break;
                }

                case "rsj":
                    var res = JsonConvert.DeserializeObject <AudioResource>(value);
                    // This can be commented out if all playlist have been written once
                    res = res.WithTitle(StringNormalize.Normalize(res.ResourceTitle));
                    items.Add(res);
                    break;

                case "id":
                case "ln":
                    Log.Warn("Deprecated playlist data block: {0}", line);
                    break;

                default:
                    Log.Warn("Erroneus playlist data block: {0}", line);
                    break;
                }
            }

            return(items);
        }
예제 #14
0
        private R <PlayResource, LocalStr> ResolveResourceInternal(AudioResource resource)
        {
            if (!WebWrapper.DownloadString(out string resulthtml, new Uri($"https://www.youtube.com/get_video_info?video_id={resource.ResourceId}")))
            {
                return(new LocalStr(strings.error_net_no_connection));
            }

            var videoTypes = new List <VideoData>();
            var dataParse  = ParseQueryString(resulthtml);

            if (dataParse.TryGetValue("player_response", out var playerData))
            {
                var parsed = JsonConvert.DeserializeObject <JsonPlayerResponse>(playerData[0]);
                Log.Debug("Extracted data: {@playerData}", parsed);

                if (parsed?.videoDetails != null)
                {
                    if (resource.ResourceTitle == null)
                    {
                        resource = resource.WithTitle(StringNormalize.Normalize(parsed.videoDetails.title));
                    }

                    bool isLive = parsed.videoDetails.isLive ?? false;
                    if (isLive && parsed.streamingData?.hlsManifestUrl != null)
                    {
                        return(ParseLiveData(resource, parsed));
                    }
                    else if (isLive)
                    {
                        Log.Warn("Live stream without hls stream data");
                    }

                    ParsePlayerData(parsed, videoTypes);
                }
            }

            if (dataParse.TryGetValue("url_encoded_fmt_stream_map", out var videoDataUnsplit))
            {
                ParseEncodedFmt(videoDataUnsplit, videoTypes);
            }

            if (dataParse.TryGetValue("adaptive_fmts", out videoDataUnsplit))
            {
                ParseAdaptiveFmt(videoDataUnsplit, videoTypes);
            }

            // Validation Process

            if (videoTypes.Count <= 0)
            {
                return(new LocalStr(strings.error_media_no_stream_extracted));
            }

            int codec = SelectStream(videoTypes);

            if (codec < 0)
            {
                return(new LocalStr(strings.error_media_no_stream_extracted));
            }

            var result = ValidateMedia(videoTypes[codec]);

            if (!result.Ok)
            {
                return(result.Error);
            }

            if (resource.ResourceTitle == null)
            {
                resource = resource.WithTitle($"<YT - no title : {resource.ResourceId}>");
            }

            return(new PlayResource(videoTypes[codec].Link, resource));
        }
예제 #15
0
        private static R <PlayResource, LocalStr> YoutubeDlWrapped(AudioResource resource)
        {
            R <JsonYtdlDump, LocalStr> GetResourceInfo(string id)
            {
                var result = YoutubeDlHelper.GetSingleVideo(id);

                if (!result.Ok)
                {
                    return(result.Error);
                }

                return(result.Value);
            }

            Log.Debug("Falling back to youtube-dl!");

            // Get first try information
            var resourceInfo = GetResourceInfo(resource.ResourceId);

            if (!resourceInfo.Ok)
            {
                return(resourceInfo.Error);
            }
            var response = resourceInfo.Value;

            var formats = YoutubeDlHelper.SortBest(response.formats);

            if (formats.Count == 0)
            {
                return(new LocalStr(strings.error_ytdl_empty_response));
            }
            string url = formats[0]?.url;

            Log.Trace("Found the following audio codec possibilities:");
            foreach (var f in formats)
            {
                if (f.acodec != "none")
                {
                    Log.Trace("Result: abr={0} acodec={1} vcodec={2} url={3}", f.abr, f.acodec, f.vcodec, f.url);
                }
            }
            Log.Trace("Using version with {0}kbit/s", formats[0]?.abr);

            // Resource is broken
            if (string.IsNullOrEmpty(url))
            {
                return(new LocalStr(strings.error_ytdl_empty_response));
            }

            // Check if URL actually works. If not, return an error.
            var resp = new HttpClient().GetAsync(url, HttpCompletionOption.ResponseHeadersRead).Result;

            if (!resp.IsSuccessStatusCode)
            {
                Log.Info("Download URL generated by youtube-dl responds with non-200 status code: " + resp.StatusCode);
                return(new LocalStr("Youtube-URL for song is broken."));
            }

            if (resource.ResourceTitle == null)
            {
                resource = resource.WithTitle(StringNormalize.Normalize(response.title) ?? $"Youtube-{resource.ResourceId}");
            }


            Log.Debug("youtube-dl succeeded!");
            return(new PlayResource(url, resource));
        }
예제 #16
0
        public R <PlayResource, LocalStr> GetResourceById(ResolveContext _, AudioResource resource)
        {
            var channel = resource.ResourceId;

            // request api token
            if (!WebWrapper.DownloadString(out string jsonResponse, new Uri($"https://api.twitch.tv/api/channels/{channel}/access_token"), ("Client-ID", TwitchClientIdPrivate)))
            {
                return(new LocalStr(strings.error_net_no_connection));
            }

            JsonAccessToken access;

            try
            {
                access = JsonConvert.DeserializeObject <JsonAccessToken>(jsonResponse);
            }
            catch (Exception ex)
            {
                Log.Debug(ex, "Failed to parse jsonResponse. (Data: {0})", jsonResponse);
                return(new LocalStr(strings.error_media_internal_invalid + " (jsonResponse)"));
            }

            // request m3u8 file
            if (access.token is null || access.sig is null)
            {
                return(new LocalStr(strings.error_media_internal_invalid + " (tokenResult|sigResult)"));
            }
            var token = Uri.EscapeUriString(access.token);
            var sig   = access.sig;
            // guaranteed to be random, chosen by fair dice roll.
            const int random = 4;

            if (!WebWrapper.DownloadString(out string m3u8, new Uri($"http://usher.twitch.tv/api/channel/hls/{channel}.m3u8?player=twitchweb&&token={token}&sig={sig}&allow_audio_only=true&allow_source=true&type=any&p={random}")))
            {
                return(new LocalStr(strings.error_net_no_connection));
            }

            // parse m3u8 file
            var dataList = new List <StreamData>();

            using (var reader = new System.IO.StringReader(m3u8))
            {
                var header = reader.ReadLine();
                if (string.IsNullOrEmpty(header) || header != "#EXTM3U")
                {
                    return(new LocalStr(strings.error_media_internal_missing + " (m3uHeader)"));
                }

                while (true)
                {
                    var blockInfo = reader.ReadLine();
                    if (string.IsNullOrEmpty(blockInfo))
                    {
                        break;
                    }

                    var match = M3U8ExtMatch.Match(blockInfo);
                    if (!match.Success)
                    {
                        continue;
                    }

                    switch (match.Groups[1].Value)
                    {
                    case "EXT-X-TWITCH-INFO": break;                     // Ignore twitch info line

                    case "EXT-X-MEDIA":
                        string streamInfo = reader.ReadLine();
                        Match  infoMatch;
                        if (string.IsNullOrEmpty(streamInfo) ||
                            !(infoMatch = M3U8ExtMatch.Match(streamInfo)).Success ||
                            infoMatch.Groups[1].Value != "EXT-X-STREAM-INF")
                        {
                            return(new LocalStr(strings.error_media_internal_missing + " (m3uStream)"));
                        }

                        var streamData = new StreamData();
                        // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,CODECS="mp4a.40.2",VIDEO="audio_only"
                        for (int i = 0; i < infoMatch.Groups[3].Captures.Count; i++)
                        {
                            string key   = infoMatch.Groups[4].Captures[i].Value.ToUpperInvariant();
                            string value = infoMatch.Groups[5].Captures[i].Value;

                            switch (key)
                            {
                            case "BANDWIDTH": streamData.Bandwidth = int.Parse(value, CultureInfo.InvariantCulture); break;

                            case "CODECS": streamData.Codec = TextUtil.StripQuotes(value); break;

                            case "VIDEO":
                                streamData.QualityType = Enum.TryParse(TextUtil.StripQuotes(value), out StreamQuality quality)
                                                                 ? quality
                                                                 : StreamQuality.unknown; break;
                            }
                        }

                        streamData.Url = reader.ReadLine();
                        dataList.Add(streamData);
                        break;
                    }
                }
            }

            // Validation Process

            if (dataList.Count <= 0)
            {
                return(new LocalStr(strings.error_media_no_stream_extracted));
            }

            int codec = SelectStream(dataList);

            if (codec < 0)
            {
                return(new LocalStr(strings.error_media_no_stream_extracted));
            }

            if (resource.ResourceTitle == null)
            {
                resource = resource.WithTitle($"Twitch channel: {StringNormalize.Normalize(channel)}");
            }
            return(new PlayResource(dataList[codec].Url, resource));
        }