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))); }
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))); }
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)); }
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)); }
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))); }
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))); }
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()); }
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 } })); }
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); }
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))); }
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); }
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()); }
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); }
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)); }
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)); }
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)); }