From 74bb9d7986bfee621cf178f761f4aaed2c6dbc52 Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sat, 2 Apr 2022 17:34:35 +0200 Subject: [PATCH 1/8] chore: Update Xamarin Essentials reference --- src/Ch9/Ch9.UWP/Ch9.UWP.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ch9/Ch9.UWP/Ch9.UWP.csproj b/src/Ch9/Ch9.UWP/Ch9.UWP.csproj index 68e4fe58..42b727aa 100644 --- a/src/Ch9/Ch9.UWP/Ch9.UWP.csproj +++ b/src/Ch9/Ch9.UWP/Ch9.UWP.csproj @@ -145,7 +145,7 @@ - + From 89b1db3bcfa4068446b1b7ab84de24ae9f21903c Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 10:27:36 +0200 Subject: [PATCH 2/8] feat: Add JsonReader to retrieve videos --- src/Ch9/Ch9.Shared/Framework/HttpUtility.cs | 30 +++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Ch9/Ch9.Shared/Framework/HttpUtility.cs b/src/Ch9/Ch9.Shared/Framework/HttpUtility.cs index 85097c68..f937918f 100644 --- a/src/Ch9/Ch9.Shared/Framework/HttpUtility.cs +++ b/src/Ch9/Ch9.Shared/Framework/HttpUtility.cs @@ -2,31 +2,32 @@ using System.Net.Http; using System.Threading.Tasks; using System.Xml; +using Newtonsoft.Json; namespace Ch9 { internal static class HttpUtility { - internal static HttpClient HttpClient { get; } = CreateHttpClient(); + internal static HttpClient HttpClient { get; } = CreateHttpClient(); internal static HttpClient CreateHttpClient() { #if __WASM__ var httpClient = new HttpClient(new Uno.UI.Wasm.WasmHttpHandler()); - httpClient.DefaultRequestHeaders.Add("origin", ""); + httpClient.DefaultRequestHeaders.Add("origin", ""); #else - var httpClient = new HttpClient(); + var httpClient = new HttpClient(); #endif - return httpClient; - } + return httpClient; + } - internal static async Task GetXmlReader(string url) + internal static async Task GetXmlReader(string url) { #if __WASM__ - url = "https://ch9-app.azurewebsites.net/api/proxy?url=" + url; + url = "https://ch9-app.azurewebsites.net/api/proxy?url=" + url; #endif - using (var response = await HttpClient.GetAsync(url)) + using (var response = await HttpClient.GetAsync(url)) { response.EnsureSuccessStatusCode(); var bytes = await response.Content.ReadAsByteArrayAsync(); @@ -34,5 +35,18 @@ internal static async Task GetXmlReader(string url) return XmlReader.Create(stream); } } + + internal static async Task GetJsonReader(string url) + { + using (var response = await HttpClient.GetAsync(url)) + { + response.EnsureSuccessStatusCode(); + var bytes = await response.Content.ReadAsByteArrayAsync(); + var stream = new MemoryStream(bytes); + TextReader reader = new StreamReader(stream); + JsonTextReader jsonReader = new JsonTextReader(reader); + return jsonReader; + } + } } } From 912192e717b1ffb4ae0a405122abd191a34821d5 Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 11:01:24 +0200 Subject: [PATCH 3/8] feat: Add feed parsing --- src/Ch9/Ch9.Shared/Domain/ShowService.cs | 198 +++++++++++++++++++---- 1 file changed, 166 insertions(+), 32 deletions(-) diff --git a/src/Ch9/Ch9.Shared/Domain/ShowService.cs b/src/Ch9/Ch9.Shared/Domain/ShowService.cs index 3d18a52d..ed78f0bf 100644 --- a/src/Ch9/Ch9.Shared/Domain/ShowService.cs +++ b/src/Ch9/Ch9.Shared/Domain/ShowService.cs @@ -14,6 +14,7 @@ using Newtonsoft.Json; using Uno.Extensions; using Uno.Logging; +using System.Globalization; namespace Ch9 { @@ -21,19 +22,20 @@ public class ShowService : IShowService { private const string YahooNamespace = "http://search.yahoo.com/mrss/"; private const string ITunesNamespace = "http://www.itunes.com/dtds/podcast-1.0.dtd"; - private static readonly SourceFeed _channel9Feed = new SourceFeed("https://s.ch9.ms/feeds/rss", "Channel 9"); + // private static readonly SourceFeed _channel9Feed = new SourceFeed("https://s.ch9.ms/feeds/rss", "Channel 9"); + private static readonly SourceFeed _channel9Feed = new SourceFeed("https://learntvpublicschedule.azureedge.net/public/schedule.json", "Microsoft Learn Shows (ex-Channel 9)"); private readonly IDictionary _cache = new Dictionary(); - private readonly HttpClient _httpClient; + private readonly HttpClient _httpClient; - public ShowService(HttpClient httpClient) + public ShowService(HttpClient httpClient) { - _httpClient = httpClient; - } + _httpClient = httpClient; + } /// - private IEnumerable GetFallbackShowFeeds() + private IEnumerable GetFallbackShowFeeds() { return new List { @@ -59,33 +61,33 @@ private IEnumerable GetFallbackShowFeeds() public async Task> GetShowFeeds() { - // If any exception occurs, fallback to the list of hardcoded shows - try - { - var response = await _httpClient.GetStringAsync("api/rssfeeds"); - - return JsonConvert.DeserializeObject(response); - } - catch (Exception e) - { - if (!IsInternetAvailable()) throw; - - this.Log().Warn("Couldn't load the shows. Fallbacking on the default shows.", e); - return GetFallbackShowFeeds(); - } - - bool IsInternetAvailable() - { - var profile = NetworkInformation.GetInternetConnectionProfile(); - var level = profile?.GetNetworkConnectivityLevel(); - return level == NetworkConnectivityLevel.InternetAccess; - } - } + // If any exception occurs, fallback to the list of hardcoded shows + try + { + var response = await _httpClient.GetStringAsync("api/rssfeeds"); + + return JsonConvert.DeserializeObject(response); + } + catch (Exception e) + { + if (!IsInternetAvailable()) throw; + + this.Log().Warn("Couldn't load the shows. Fallbacking on the default shows.", e); + return GetFallbackShowFeeds(); + } + + bool IsInternetAvailable() + { + var profile = NetworkInformation.GetInternetConnectionProfile(); + var level = profile?.GetNetworkConnectivityLevel(); + return level == NetworkConnectivityLevel.InternetAccess; + } + } /// public async Task GetShow(SourceFeed sourceFeed = null) { - var url = sourceFeed != null ? sourceFeed.FeedUrl : _channel9Feed.FeedUrl; + var url = sourceFeed != null ? sourceFeed.FeedUrl : _channel9Feed.FeedUrl; if (_cache.TryGetValue(url, out var cachedShow)) { @@ -129,9 +131,141 @@ private IEnumerable GetEpisodes(SourceFeed sourceFeed, SyndicationFeed private async Task GetRssFeed(string url) { - using (var reader = await HttpUtility.GetXmlReader(url)) + using (var reader = await HttpUtility.GetJsonReader(url)) + { + var feedItems = await ReadFeed(reader); + SyndicationFeed feed = new SyndicationFeed(feedItems); + feed.Description = SyndicationContent.CreatePlaintextContent("Microsoft Learn TV"); + return feed; + } + } + + private static async Task> ReadFeed(JsonTextReader reader) + { + List feedItems = new List(); + SyndicationItem currentItem = null; + String currentProperty = null; + DateTimeOffset? startTime = null; + DateTimeOffset? endTime = null; + Boolean? isLive = null; + while (await reader.ReadAsync()) + { + switch (reader.TokenType) + { + case JsonToken.StartObject: + currentItem = new SyndicationItem(); + break; + case JsonToken.EndObject: + CompleteFeedItem(currentItem, startTime, endTime, isLive); + + startTime = null; + endTime = null; + isLive = null; + + feedItems.Add(currentItem); + break; + case JsonToken.PropertyName: + currentProperty = reader.Value.ToString(); + break; + case JsonToken.Integer: + if(currentProperty == "pubble_id") + { + currentItem.Id = reader.Value.ToString(); + } + break; + case JsonToken.String: + switch (currentProperty) + { + case "title": { + currentItem.Title = SyndicationContent.CreatePlaintextContent($"|{reader.Value.ToString()}"); + break; + } + case "description": { + currentItem.Summary = SyndicationContent.CreatePlaintextContent(reader.Value.ToString()); + break; + } + case "externalurl": { + currentItem.BaseUri = new Uri(reader.Value.ToString()); + break; + } + default: + break; + } + break; + case JsonToken.Boolean: + if (currentProperty == "islive") + { + isLive = (Boolean)reader.Value; + } + break; + case JsonToken.Date: + switch (currentProperty) + { + case "start_time": + { + startTime = DateTimeOffset.Parse(reader.Value.ToString()); + currentItem.PublishDate = startTime.Value; + break; + } + case "end_time": + { + endTime = DateTimeOffset.Parse(reader.Value.ToString()); + break; + } + default: + break; + } + break; + // Commented out because not necessary, but useful for future improvements + //case JsonToken.Undefined: + // break; + //case JsonToken.None: + // break; + //case JsonToken.Null: + // break; + //case JsonToken.StartArray: + // break; + //case JsonToken.EndArray: + // break; + //case JsonToken.StartConstructor: + // break; + //case JsonToken.Float: + // break; + //case JsonToken.Comment: + // break; + //case JsonToken.Raw: + // break; + //case JsonToken.EndConstructor: + // break; + //case JsonToken.Bytes: + // break; + default: + break; + } + } + + return feedItems; + } + + private static void CompleteFeedItem(SyndicationItem currentItem, DateTimeOffset? startTime, DateTimeOffset? endTime, bool? isLive) + { + String details = ""; + if (isLive != null) + { + details += $"Is Live Now: {isLive.Value}
\\n"; + } + if (startTime != null) + { + details += $"Starts At: {startTime.Value.ToString(CultureInfo.CurrentCulture)}
\\n"; + } + if (endTime != null) + { + details += $"Ends At: {endTime.Value.ToString(CultureInfo.CurrentCulture)}
\\n"; + } + + if (!String.IsNullOrWhiteSpace(details)) { - return SyndicationFeed.Load(reader); + currentItem.Content = SyndicationContent.CreateHtmlContent(details); } } @@ -140,7 +274,7 @@ private Episode CreateEpisode(SyndicationItem item, SourceFeed sourceFeed) return new Episode { Title = GetTitle(item), - Show = GetShow(item, sourceFeed), + Show = GetShow(item, sourceFeed), Summary = GetSummary(item), Date = item.PublishDate, Categories = GetCategories(item).ToArray(), From 95d7dfbe058bb729011dbe1144eb76a2c7d34fd3 Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 12:42:01 +0200 Subject: [PATCH 4/8] chore: Restore changed package version --- src/Ch9/Ch9.UWP/Ch9.UWP.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ch9/Ch9.UWP/Ch9.UWP.csproj b/src/Ch9/Ch9.UWP/Ch9.UWP.csproj index 42b727aa..cf7d1a9a 100644 --- a/src/Ch9/Ch9.UWP/Ch9.UWP.csproj +++ b/src/Ch9/Ch9.UWP/Ch9.UWP.csproj @@ -132,7 +132,7 @@ you need to make sure that the version provided here matches https://github.com/onovotny/MSBuildSdkExtras/blob/master/Source/MSBuild.Sdk.Extras/DefaultItems/ImplicitPackages.targets#L11. This is not an issue when libraries are referenced through nuget packages. See https://github.com/unoplatform/uno/issues/446 for more details. --> - 6.2.8 + 6.2.9
From dd649362e94d83d91ccb0533ad91fb2d349218aa Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 12:45:14 +0200 Subject: [PATCH 5/8] chore: Change Android destination framework version --- src/Ch9/Ch9.Droid/Ch9.Droid.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ch9/Ch9.Droid/Ch9.Droid.csproj b/src/Ch9/Ch9.Droid/Ch9.Droid.csproj index 03b547b2..dc7de6b0 100644 --- a/src/Ch9/Ch9.Droid/Ch9.Droid.csproj +++ b/src/Ch9/Ch9.Droid/Ch9.Droid.csproj @@ -17,7 +17,7 @@ true Off False - v10.0 + v11.0 Properties\AndroidManifest.xml True ..\Ch9.Shared\Strings From 43e23f0ccbd873a2a4086136f1d7eeb82f38d1e0 Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 13:14:51 +0200 Subject: [PATCH 6/8] feat: Create list of episodes --- src/Ch9/Ch9.Shared/Domain/ShowService.cs | 33 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Ch9/Ch9.Shared/Domain/ShowService.cs b/src/Ch9/Ch9.Shared/Domain/ShowService.cs index ed78f0bf..026d5957 100644 --- a/src/Ch9/Ch9.Shared/Domain/ShowService.cs +++ b/src/Ch9/Ch9.Shared/Domain/ShowService.cs @@ -24,6 +24,8 @@ public class ShowService : IShowService private const string ITunesNamespace = "http://www.itunes.com/dtds/podcast-1.0.dtd"; // private static readonly SourceFeed _channel9Feed = new SourceFeed("https://s.ch9.ms/feeds/rss", "Channel 9"); private static readonly SourceFeed _channel9Feed = new SourceFeed("https://learntvpublicschedule.azureedge.net/public/schedule.json", "Microsoft Learn Shows (ex-Channel 9)"); + private const string LearnTvLogo = "https://static.docs.com/third-party/learn-player/v1.0.0/images/Learn_Thumbnail_v2_1920x1080.jpg"; + private readonly IDictionary _cache = new Dictionary(); @@ -148,10 +150,22 @@ private static async Task> ReadFeed(JsonTextReader reader) DateTimeOffset? startTime = null; DateTimeOffset? endTime = null; Boolean? isLive = null; + bool inArray = false; // The url returns an Object with a property named "content" which is an Array while (await reader.ReadAsync()) { + if (!inArray) + { + continue; + } + switch (reader.TokenType) { + case JsonToken.StartArray: + inArray = true; + break; + case JsonToken.EndArray: + inArray = false; + break; case JsonToken.StartObject: currentItem = new SyndicationItem(); break; @@ -223,10 +237,6 @@ private static async Task> ReadFeed(JsonTextReader reader) // break; //case JsonToken.Null: // break; - //case JsonToken.StartArray: - // break; - //case JsonToken.EndArray: - // break; //case JsonToken.StartConstructor: // break; //case JsonToken.Float: @@ -266,7 +276,22 @@ private static void CompleteFeedItem(SyndicationItem currentItem, DateTimeOffset if (!String.IsNullOrWhiteSpace(details)) { currentItem.Content = SyndicationContent.CreateHtmlContent(details); + currentItem.ElementExtensions.Add(new SyndicationElementExtension("summary", ITunesNamespace, details)); + } + + TimeSpan? duration = null; + if (startTime.HasValue && endTime.HasValue) + { + duration = endTime.Value - startTime.Value; } + + currentItem.ElementExtensions.Add(new SyndicationElementExtension("duration", ITunesNamespace, duration.HasValue ? (long)duration.Value.TotalSeconds : 0L)); + currentItem.Links.Add(new SyndicationLink(currentItem.BaseUri, "alternate", currentItem.Title.Text, "", duration.HasValue ? (long)duration.Value.TotalSeconds : 0L)); + currentItem.Links.Add(new SyndicationLink(currentItem.BaseUri, "", currentItem.Title.Text, "video/mp4", duration.HasValue ? (long)duration.Value.TotalSeconds : 0L)); + + var thumbnail = new XElement(XName.Get("thumbnail")); + thumbnail.SetAttributeValue(XName.Get("url"), LearnTvLogo); + currentItem.ElementExtensions.Add("thumbnail", YahooNamespace, thumbnail); } private Episode CreateEpisode(SyndicationItem item, SourceFeed sourceFeed) From 7eb7876d23b9f20f26914150ada533f4513ba1d0 Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 15:18:15 +0200 Subject: [PATCH 7/8] feat: Complete feed parsing --- src/Ch9/Ch9.Shared/Domain/ShowService.cs | 32 +++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Ch9/Ch9.Shared/Domain/ShowService.cs b/src/Ch9/Ch9.Shared/Domain/ShowService.cs index 026d5957..2a4b2626 100644 --- a/src/Ch9/Ch9.Shared/Domain/ShowService.cs +++ b/src/Ch9/Ch9.Shared/Domain/ShowService.cs @@ -150,22 +150,30 @@ private static async Task> ReadFeed(JsonTextReader reader) DateTimeOffset? startTime = null; DateTimeOffset? endTime = null; Boolean? isLive = null; - bool inArray = false; // The url returns an Object with a property named "content" which is an Array + bool? inArray = null; + + // The url returns an Object with a property named "content" which is an Array while (await reader.ReadAsync()) { - if (!inArray) + if (inArray == null) { + if (reader.TokenType == JsonToken.StartArray) + { + inArray = true; + } + continue; + } else if (inArray == false) { + continue; + } else if (inArray == true) { + if (reader.TokenType == JsonToken.EndArray) { + inArray = false; + continue; + } } switch (reader.TokenType) { - case JsonToken.StartArray: - inArray = true; - break; - case JsonToken.EndArray: - inArray = false; - break; case JsonToken.StartObject: currentItem = new SyndicationItem(); break; @@ -230,7 +238,13 @@ private static async Task> ReadFeed(JsonTextReader reader) break; } break; - // Commented out because not necessary, but useful for future improvements + // Commented out because not necessary, but can be useful for future improvements + //case JsonToken.StartArray: + // inArray = true; + // break; + //case JsonToken.EndArray: + // inArray = false; + // break; //case JsonToken.Undefined: // break; //case JsonToken.None: From 730191e82280746d6b49d29239972487472119a0 Mon Sep 17 00:00:00 2001 From: Raffaele Terribile Date: Sun, 3 Apr 2022 16:44:24 +0200 Subject: [PATCH 8/8] feat: Change the way feed item title is calculated --- src/Ch9/Ch9.Shared/Domain/ShowService.cs | 25 ++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Ch9/Ch9.Shared/Domain/ShowService.cs b/src/Ch9/Ch9.Shared/Domain/ShowService.cs index 2a4b2626..a095d4ec 100644 --- a/src/Ch9/Ch9.Shared/Domain/ShowService.cs +++ b/src/Ch9/Ch9.Shared/Domain/ShowService.cs @@ -147,6 +147,8 @@ private static async Task> ReadFeed(JsonTextReader reader) List feedItems = new List(); SyndicationItem currentItem = null; String currentProperty = null; + String title = null; + String externalTitle = null; DateTimeOffset? startTime = null; DateTimeOffset? endTime = null; Boolean? isLive = null; @@ -178,7 +180,7 @@ private static async Task> ReadFeed(JsonTextReader reader) currentItem = new SyndicationItem(); break; case JsonToken.EndObject: - CompleteFeedItem(currentItem, startTime, endTime, isLive); + CompleteFeedItem(currentItem, title, externalTitle, startTime, endTime, isLive); startTime = null; endTime = null; @@ -199,7 +201,11 @@ private static async Task> ReadFeed(JsonTextReader reader) switch (currentProperty) { case "title": { - currentItem.Title = SyndicationContent.CreatePlaintextContent($"|{reader.Value.ToString()}"); + title = reader.Value.ToString(); + break; + } + case "externaltitle": { + externalTitle = reader.Value.ToString(); break; } case "description": { @@ -271,20 +277,25 @@ private static async Task> ReadFeed(JsonTextReader reader) return feedItems; } - private static void CompleteFeedItem(SyndicationItem currentItem, DateTimeOffset? startTime, DateTimeOffset? endTime, bool? isLive) + private static void CompleteFeedItem(SyndicationItem currentItem, String title, String externalTitle, DateTimeOffset? startTime, DateTimeOffset? endTime, bool? isLive) { + if (title == null) + { + title = externalTitle; + } + String details = ""; if (isLive != null) { - details += $"Is Live Now: {isLive.Value}
\\n"; + details += $"Is Live Now: {isLive.Value}"; } if (startTime != null) { - details += $"Starts At: {startTime.Value.ToString(CultureInfo.CurrentCulture)}
\\n"; + details += (String.IsNullOrWhiteSpace(details) ? "" : " - ") + $"Starts At: {startTime.Value.ToString(CultureInfo.CurrentCulture)}"; } if (endTime != null) { - details += $"Ends At: {endTime.Value.ToString(CultureInfo.CurrentCulture)}
\\n"; + details += (String.IsNullOrWhiteSpace(details) ? "" : " - ") + $"Ends At: {endTime.Value.ToString(CultureInfo.CurrentCulture)}"; } if (!String.IsNullOrWhiteSpace(details)) @@ -299,6 +310,8 @@ private static void CompleteFeedItem(SyndicationItem currentItem, DateTimeOffset duration = endTime.Value - startTime.Value; } + currentItem.Title = SyndicationContent.CreatePlaintextContent($"{(startTime.HasValue ? (startTime.Value.ToString(CultureInfo.CurrentCulture) + ": ") : "")}{title}|"); + currentItem.ElementExtensions.Add(new SyndicationElementExtension("duration", ITunesNamespace, duration.HasValue ? (long)duration.Value.TotalSeconds : 0L)); currentItem.Links.Add(new SyndicationLink(currentItem.BaseUri, "alternate", currentItem.Title.Text, "", duration.HasValue ? (long)duration.Value.TotalSeconds : 0L)); currentItem.Links.Add(new SyndicationLink(currentItem.BaseUri, "", currentItem.Title.Text, "video/mp4", duration.HasValue ? (long)duration.Value.TotalSeconds : 0L));