Functions/GenXdev.Console.Spotify/Get-SpotifyLyrics.ps1

<##############################################################################
Part of PowerShell module : GenXdev.Console.Spotify
Original cmdlet filename : Get-SpotifyLyrics.ps1
Original author : René Vaessen / GenXdev
Version : 1.264.2025
################################################################################
MIT License
 
Copyright 2021-2025 GenXdev
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
################################################################################>

###############################################################################
<#
.SYNOPSIS
Retrieves lyrics for Spotify tracks from Musixmatch.com
 
.DESCRIPTION
Searches for and displays song lyrics by either:
- Using a Spotify track ID
- Searching for tracks by name/artist
- Getting lyrics for currently playing track
If lyrics aren't found on Musixmatch, opens a Google search as fallback.
 
.PARAMETER TrackId
The Spotify track ID to look up lyrics for. If omitted, uses currently playing
track or allows searching by name.
 
.PARAMETER Queries
Search terms to find a track. Can include artist name and/or song title.
Results will be shown for selection.
 
.EXAMPLE
Get-SpotifyLyrics -TrackId "1301WleyT98MSxVHPZCA6M"
 
.EXAMPLE
lyrics "bohemian rhapsody queen"
#>

function Get-SpotifyLyrics {

    [CmdLetBinding(DefaultParameterSetName = '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Get-SpotifyLyrics')]
    [Alias('lyrics')]

    param(
        ########################################################################
        [parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName,
            ParameterSetName = '',
            HelpMessage = 'Spotify track ID to lookup lyrics for'
        )]
        [Alias('Id')]
        [string] $TrackId = $null,
        ###############################################################################

        ###############################################################################
        [Alias('incognito','inprivate')]
        [parameter(Mandatory = $false, HelpMessage = 'Open browser in private/incognito mode')]
        [switch] $Private,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Force browser launch or override restrictions')]
        [switch] $Force,
        ###############################################################################

        ###############################################################################
        [Alias('e')]
        [parameter(Mandatory = $false, HelpMessage = 'Use Microsoft Edge browser')]
        [switch] $Edge,
        ###############################################################################

        ###############################################################################
        [Alias('ch')]
        [parameter(Mandatory = $false, HelpMessage = 'Use Google Chrome browser')]
        [switch] $Chrome,
        ###############################################################################

        ###############################################################################
        [Alias('c')]
        [parameter(Mandatory = $false, HelpMessage = 'Use Chromium browser')]
        [switch] $Chromium,
        ###############################################################################

        ###############################################################################
        [Alias('ff')]
        [parameter(Mandatory = $false, HelpMessage = 'Use Mozilla Firefox browser')]
        [switch] $Firefox,
        ###############################################################################

        ###############################################################################
        [Alias('m','mon')]
        [parameter(Mandatory = $false, HelpMessage = 'Target monitor for browser window')]
        [int] $Monitor,
        ###############################################################################

        ###############################################################################
        [Alias('fs','f')]
        [parameter(Mandatory = $false, HelpMessage = 'Open browser in full screen mode')]
        [switch] $FullScreen,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Width of browser window in pixels')]
        [int] $Width,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Height of browser window in pixels')]
        [int] $Height,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Left position of browser window')]
        [int] $Left,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Right position of browser window')]
        [int] $Right,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Top position of browser window')]
        [int] $Top,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Bottom position of browser window')]
        [int] $Bottom,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Center browser window on screen')]
        [switch] $Centered,
        ###############################################################################

        ###############################################################################
        [Alias('a','app','appmode')]
        [parameter(Mandatory = $false, HelpMessage = 'Open browser in application mode')]
        [switch] $ApplicationMode,
        ###############################################################################

        ###############################################################################
        [Alias('de','ne','NoExtensions')]
        [parameter(Mandatory = $false, HelpMessage = 'Disable browser extensions')]
        [switch] $NoBrowserExtensions,
        ###############################################################################

        ###############################################################################
        [Alias('allowpopups')]
        [parameter(Mandatory = $false, HelpMessage = 'Disable popup blocker in browser')]
        [switch] $DisablePopupBlocker,
        ###############################################################################

        ###############################################################################
        [Alias('lang','locale')]
        [parameter(Mandatory = $false, HelpMessage = 'Set Accept-Language HTTP header')]
        [string] $AcceptLang,
        ###############################################################################

        ###############################################################################
        [Alias('Escape')]
        [parameter(Mandatory = $false, HelpMessage = 'Send Escape key after page load')]
        [switch] $SendKeyEscape,
        ###############################################################################

        ###############################################################################
        [Alias('HoldKeyboardFocus')]
        [parameter(Mandatory = $false, HelpMessage = 'Hold keyboard focus after sending keys')]
        [switch] $SendKeyHoldKeyboardFocus,
        ###############################################################################

        ###############################################################################
        [Alias('UseShiftEnter')]
        [parameter(Mandatory = $false, HelpMessage = 'Use Shift+Enter when sending keys')]
        [switch] $SendKeyUseShiftEnter,
        ###############################################################################

        ###############################################################################
        [Alias('DelayMilliSeconds')]
        [parameter(Mandatory = $false, HelpMessage = 'Delay in milliseconds between sending keys')]
        [int] $SendKeyDelayMilliSeconds,
        ###############################################################################

        ###############################################################################
        [Alias('fw','focus')]
        [parameter(Mandatory = $false, HelpMessage = 'Focus browser window after launch')]
        [switch] $FocusWindow,
        ###############################################################################

        ###############################################################################
        [Alias('fg')]
        [parameter(Mandatory = $false, HelpMessage = 'Set browser window to foreground')]
        [switch] $SetForeground,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Maximize browser window')]
        [switch] $Maximize,
        ###############################################################################

        ###############################################################################
        [Alias('rf','bg')]
        [parameter(Mandatory = $false, HelpMessage = 'Restore focus to previous window after closing browser')]
        [switch] $RestoreFocus,
        ###############################################################################

        ###############################################################################
        [Alias('nw','new')]
        [parameter(Mandatory = $false, HelpMessage = 'Open link in a new browser window')]
        [switch] $NewWindow,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Return the URL after opening')]
        [switch] $ReturnURL,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Return only the URL, do not open browser')]
        [switch] $ReturnOnlyURL,
        ###############################################################################

        ###############################################################################
        [Alias('nb')]
        [parameter(Mandatory = $false, HelpMessage = 'Open browser window without borders')]
        [switch] $NoBorders,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Use session-only mode for browser')]
        [switch] $SessionOnly,
        ###############################################################################
        [parameter(Mandatory = $false, HelpMessage = 'Clear browser session before opening')]
        [switch] $ClearSession,
        ###############################################################################

        ###############################################################################
        [Alias('FromPreferences')]
        [parameter(Mandatory = $false, HelpMessage = 'Skip using browser session')]
        [switch] $SkipSession,
        ###############################################################################
        ###############################################################################
        [Alias('sbs')]
        [parameter(Mandatory = $false, HelpMessage = 'Open browser side by side with other windows')]
        [switch] $SideBySide
        ###############################################################################
    )

    begin {

        if (@(Microsoft.PowerShell.Core\Get-Module GenXdev.Queries -ErrorAction SilentlyContinue).Count -eq 0) {

            $null = PowerShellGet\Install-Module GenXdev.Queries -SkipPublisherCheck
            $null = Microsoft.PowerShell.Core\Import-Module GenXdev.Queries
        }

        # handle track search if queries provided
        if ($null -ne $Queries) {

            Microsoft.PowerShell.Utility\Write-Verbose "Searching Spotify for tracks matching query: $Queries"

            # search spotify and build list of track names with artists
            $results = GenXdev.Console\Search-Spotify -SearchType Track -Queries $Queries
            $new = [System.Collections.Generic.List[string]]::new()

            foreach ($track in $results.Tracks.Items) {
                $null = $new.Add("$($track.Artists[0].Name) - $($track.Name)")
            }

            $Queries = $new
            if ($new.Count -eq 0) {
                Microsoft.PowerShell.Utility\Write-Warning 'No tracks found matching search terms'
            }
        }
        else {
            # use track ID if provided
            if ([String]::IsNullOrWhiteSpace($TrackId) -eq $false) {

                Microsoft.PowerShell.Utility\Write-Verbose "Looking up track by ID: $TrackId"
                $track = GenXdev.Console\Get-SpotifyTrackById -TrackId $TrackId

                if ($null -ne $track) {
                    $Queries = @("$($track.Artists[0].Name) - $($track.Name)")
                }
            }
            else {
                # get currently playing track
                Microsoft.PowerShell.Utility\Write-Verbose 'Getting currently playing track'
                $current = GenXdev.Console\Get-SpotifyCurrentlyPlaying

                if ($null -ne $current) {
                    $Queries = @("$($current.Item.Artists[0].Name) - " +
                        "$($current.Item.Name)")
                }
            }
        }

        if ($null -eq $Queries) {
            throw 'No song playing and no search terms provided'
        }

        if (@(Microsoft.PowerShell.Core\Get-Module GenXdev.Queries -ErrorAction SilentlyContinue).Count -eq 0) {

            $null = PowerShellGet\Install-Module GenXdev.Queries -SkipPublisherCheck
            $null = Microsoft.PowerShell.Core\Import-Module GenXdev.Queries
        }

        $params = GenXdev.Helpers\Copy-IdenticalParamValues `
            -FunctionName 'GenXdev.Queries\Open-GoogleQuery' `
            -BoundParameters $PSBoundParameters `
            -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue)
    }


    process {

        foreach ($query in $Queries) {
            Microsoft.PowerShell.Utility\Write-Verbose "Searching Musixmatch for lyrics: $query"

            # encode query for URL
            $q = [Uri]::EscapeUriString($query)
            [string] $html = ''

            # attempt to get search results page
            try {
                $html = Microsoft.PowerShell.Utility\Invoke-WebRequest `
                    -Uri "https://www.musixmatch.com/search/$q" `
                    -ErrorAction SilentlyContinue
            }
            catch {
                Microsoft.PowerShell.Utility\Write-Warning "No results found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            # extract best match URL from search results
            [int] $idx = $html.IndexOf('Best Result')
            if ($idx -lt 0) {
                Microsoft.PowerShell.Utility\Write-Warning "No results found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            $idx = $html.IndexOf('<a class="title" href="', $idx)
            if ($idx -lt 0) {
                Microsoft.PowerShell.Utility\Write-Warning "No results found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            $idx += '<a class="title" href="'.Length
            [int] $idx2 = $html.IndexOf('"', $idx)

            if ($idx2 -lt 0) {
                Microsoft.PowerShell.Utility\Write-Warning "No results found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            # get lyrics page
            $url = "https://www.musixmatch.com$($html.Substring($idx, $idx2-$idx))"
            Microsoft.PowerShell.Utility\Write-Verbose "Fetching lyrics from: $url"

            try {
                $html = Microsoft.PowerShell.Utility\Invoke-WebRequest -Uri $url -ErrorAction SilentlyContinue
            }
            catch {
                Microsoft.PowerShell.Utility\Write-Warning "Failed to get lyrics for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            # extract lyrics from page
            $idx = $html.IndexOf('"body":"')
            if ($idx -lt 0) {
                Microsoft.PowerShell.Utility\Write-Warning "No lyrics found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            $idx += '"body":"'.Length
            $idx2 = $html.IndexOf('","language":', $idx)

            if ($idx2 -lt 0) {
                Microsoft.PowerShell.Utility\Write-Warning "No lyrics found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
                continue
            }

            # parse and clean up lyrics
            $result = "`"$($html.Substring($idx, $idx2-$idx))`"" |
                Microsoft.PowerShell.Utility\ConvertFrom-Json

            if ([String]::IsNullOrWhiteSpace($result)) {
                Microsoft.PowerShell.Utility\Write-Warning "Empty lyrics found for '$query'"
                GenXdev.Queries\Open-GoogleQuery @params -Queries "lyrics $query"
            }

            $result.Replace('???', "'")
        }
    }
}