A few days ago, someone on Twitter humorously lamented the fact that I expected them to actually read a blog post. After the laughter subsided I thought, well why does he have to? Perhaps I can make it easier for him. Plus I needed something fun for today. So I put together a PowerShell function I call Invoke-BlogReader which I think you'll have fun playing with. It isn't 100% perfect and as with most Friday Fun posts, serves more as an educational device than anything.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The function uses the .NET System.Speech class which seems to be a bit easier to use than legacy COM alternative. Here's a snippet you can test.
Add-Type -AssemblyName System.speech $voice = New-Object System.Speech.Synthesis.SpeechSynthesizer $voice.Speak("Hello. I am $($voice.voice.Name)")
So basically, all I have to do is get the contents of a blog article and pass the text to the voice object. That is a bit easier said than done which you'll see as you look through this code.
#Requires -version 3.0 Function Invoke-BlogReader { [cmdletbinding()] Param( [Parameter(Position=0,Mandatory,HelpMessage="Enter the blog post URL")] [ValidateNotNullorEmpty()] [string]$Url, [ValidateScript({$_ -ge 1})] [int]$Count = 5, [switch]$ShowText, [validateSet("Male","Female")] [string]$Gender="Male" ) Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" Try { Write-Verbose "Requesting $url" $web = Invoke-WebRequest -Uri $url -DisableKeepAlive -ErrorAction Stop Write-Verbose "Selecting content" $content = $web.ParsedHtml.getElementsByTagName("div") | where {$_.classname -match'entry-content|post-content'} | Select -first 1 } Catch { Write-Warning "Failed to retrieve content from $url" #bail out Return } if ($content) { #define a regex to match sentences [regex]$rx="(\S.+?[.!?])(?=\s+|$)" Write-Verbose "Parsing sentences" $sentences = $rx.matches($content.innertext) Write-Verbose "Adding System.Speech assembly" Add-Type -AssemblyName System.speech Write-Verbose "Initializing a voice" $voice = New-Object System.Speech.Synthesis.SpeechSynthesizer $voice.SelectVoiceByHints($gender) Write-Verbose "Speaking" $voice.Speak($($web.ParsedHtml.title)) $voice.speak("Published on $($web.ParsedHtml.lastModified)") Write-Verbose "Here are the first $count lines" for ($i=0 ;$i -lt $count;$i++) { $text = $sentences[$i].Value if ($ShowText) { write-host "$text " -NoNewline } $voice.speak($text) } write-host "`n" } else { Write-Warning "No web content found" } #clean up $voice.Dispose() Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } #end function
The function uses the Invoke-WebRequest cmdlet to retrieve the content from the specified web page and I then parse the HTML looking for the content.
$web = Invoke-WebRequest -Uri $url -DisableKeepAlive -ErrorAction Stop Write-Verbose "Selecting content" $content = $web.ParsedHtml.getElementsByTagName("div") | where {$_.classname -match'entry-content|post-content'} | Select -first 1
This is the trickiest part because different blogs and sites use different tags and classes. There is a getElementsbyClassName method, but that seems to be hit and miss for me, so I've opted for the slower but consistent process of using Where-Object.
Once I have the content, I could simply pass the text, but I realized I may not want to listen to the entire post. Especially for my own which often have script samples. Those aren't very pleasant to listen to. So I realized I needed to parse the content into sentences. Regular expressions to the rescue.
[regex]$rx="(\S.+?[.!?])(?=\s+|$)" Write-Verbose "Parsing sentences" $sentences = $rx.matches($content.innertext)
Don't ask me to explain the regex pattern. I "found" it and it works. That's all that matters. $Sentences is an array of regex match objects. All I need to do is pass the value from each match to the voice object. The other benefit is that I can include a parameter to also display the text as it is being read.
for ($i=0 ;$i -lt $count;$i++) { $text = $sentences[$i].Value if ($ShowText) { write-host "$text " -NoNewline } $voice.speak($text) }
That's all there is to it! If you want to try out all of the options here's a sample command:
$paramHash = @{ Url = "http://blogs.msdn.com/b/powershell/archive/2014/08/27/powershell-summit-europe-2014.aspx" Count = 5 Gender = "Female" ShowText = $True Verbose = $True } Invoke-BlogReader @paramHash
Where this can get really fun is using another function, which I'm not sure I ever posted here, to get items from an RSS feed.
#requires -version 3.0 Function Get-RSSFeed { Param ( [Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] [ValidateNotNullOrEmpty()] [ValidatePattern("^http")] [Alias('url')] [string[]]$Path="http://feeds.feedburner.com/JeffsScriptingBlogAndMore" ) Begin { Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" } #begin Process { foreach ($item in $path) { $data = Invoke-RestMethod -Uri $item foreach ($entry in $data) { #create a custom object [pscustomobject][ordered]@{ Title = $entry.title Published = $entry.pubDate -as [datetime] Link = $entry.origLink } #hash } #foreach entry } #foreach item } #process End { Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } #end } #end Get-RSSFeed
Putting it all together you can get the feed, pipe it to Out-Gridview, select one and have the blog read to you!
get-rssfeed | out-gridview -OutputMode Single | foreach { Invoke-BlogReader $_.link -Count 5}
So now you can have your blog and listen to! There are probably a number of ways this could be enhanced. Or perhaps you want to take some of these concepts and techniques in another direction. If so, I hope you'll let me know where you end up. Have a great weekend.
The wife had a blast as I started making the computer read off things to her, like $voice.Speak(“What’s for dinner!!”)
She asks; can you make it do the dishes!? 😀
Very nice – I think I will wrap in a form with little buttons to click. I can then stage up a number of bogs articles and listen while making dinner or exercising.
Thanks.