films.ps1

using namespace System.Collections.Generic

### Includes:
. "$PSScriptRoot\common.ps1"

if (0) {
  . "$PSScriptRoot\kinopoisk.ps1"
}

### Types:
class FilmInfo {
  [string]$Name
  [int]$Year
  [string]$Resolution
  [string]$Source
  [string]$DynamicRange
  [string]$Codec
  [string[]]$Unknown = @()
  [string]$Container
}

### Variables:

$kodi_nfo_template = @"
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<movie>
  <title/>
  <originaltitle/>
  <year/>
  <plot/>
  <mpaa/>
</movie>
"@


#??? tagline = shortDescription !!! ломает сканирование

### Functions:

function Parse-FileName {
  [CmdletBinding()]
  [OutputType([FilmInfo])]
  param (
    [Parameter(Mandatory = $true)]
    [string]$Name
  )
  
  $config = Get-Config
  $media_params = $config.FileNameTokens
  
  $result = [FilmInfo]::new()
  $film_name_done = $false
  
  $containers = @(
    [io.path]::GetExtension($Name).Trim('.')
  )
  
  $Name.Replace('[', "`n[").Replace(']', "]`n").Replace('(', "`n[").Replace(')', "]`n").Split("`n", [StringSplitOptions]::RemoveEmptyEntries) | % {
    if ($_.StartsWith('[')) {
      $_
    } else {
      $_ -split ' ' | % { "$_".Trim() } | ? { $_ }
    }
  } | % {
    if ($_.StartsWith('[')) {
      $_
    } elseif ($_ -in $media_params.ExcludeSplit) {
      $_
    } else {
      $_ -split '\.' | % { "$_".Trim() } | ? { $_ }
    }
  } | % { "$_".Trim() } | ? { $_ } | % {
    if ($_.StartsWith('[')) {
      [pscustomobject]@{
        Value   = $_.TrimStart('[').TrimEnd(']')
        Bracket = $true
      }
    } else {
      [pscustomobject]@{
        Value   = $_
        Bracket = $false
      }
    }
  } | % {
    
    if (!$film_name_done) {
      if (($_.Value -notmatch '^\d{4}$') -and (!$_.Bracket)) {
        $result.Name += $_.Value + ' '
        
      } else {
        if ($_.Value -match '^\d{4}$') {
          $result.Year = $_.Value
        }
        $film_name_done = $true
      }
      
    } elseif ($_.Value -match '^\d{4}$') {
      $result.Year = $_.Value
    } elseif ($_.Value -in $media_params.Resolutions) {
      $result.Resolution = $_.Value
    } elseif ($_.Value -in $media_params.Sources) {
      $result.Source = $_.Value
    } elseif ($_.Value -in $containers) {
      $result.Container = $_.Value
    } elseif ($_.Value -in $media_params.DynamicRanges) {
      $result.DynamicRange = $_.Value
    } elseif ($_.Value -in $media_params.Codecs) {
      $result.Codec = $_.Value
    } else {
      $result.Unknown += $_.Value
    }
    
  }
  
  $result.Name = $result.Name.Trim()
  
  return $result
}

function Export-KodiNfo {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$VideoFilePath,
    [Alias('KinopoiskInfo')]
    $kp_info
  )
  
  Write-Verbose "Export-KodiNfo: begin"
  
  $xml = [xml]$kodi_nfo_template
  $doc = $xml.DocumentElement
  
  $doc.title = $kp_info.name
  $doc.originaltitle = $kp_info.alternativeName
  $doc.year = [string]$kp_info.year
  $doc.plot = $kp_info.description
  
  # ??? tagline = shortDescription !!! ломает сканирование
  # $doc.tagline = $kp_info.shortDescription
  
  $age_ratings = @()
  if ($kp_info.ageRating) {
    $age_ratings += "$($kp_info.ageRating)+"
  }
  if ($kp_info.ratingMpaa) {
    $age_ratings += "$($kp_info.ratingMpaa)".ToUpper()
  }
  $doc.mpaa = $age_ratings -join ' / '
  
  # $doc.mpaa = "$($kp_info.ratingMpaa)".ToUpper()
  # if ($kp_info.ageRating) {
  # $doc.mpaa = "$($kp_info.ageRating)+" + " / " + $doc.mpaa
  # }
  
  ### Ratings
    <#
        <rating name="imdb" max="10" default="true">
            <value>7.600000</value>
            <votes>471131</votes>
        </rating>
  #>
  
  $ratings_node = $doc.AppendChild($xml.CreateElement("ratings"))
  
  if ($kp_info.rating.kp) {
    $rating_node = [Xml.XmlElement]$ratings_node.AppendChild($xml.CreateElement("rating"))
    $rating_node.SetAttribute('name', 'kinopoisk')
    $rating_node.SetAttribute('max', '10')
    $rating_node.SetAttribute('default', 'true')
    $rating_node.AppendChild($xml.CreateElement("value")).InnerText = ("{0:f1}" -f $kp_info.rating.kp).Replace(',', '.')
    $rating_node.AppendChild($xml.CreateElement("votes")).InnerText = ("{0:f0}" -f $kp_info.votes.kp).Replace(',', '.')
  }
  
  if ($kp_info.rating.imdb) {
    $rating_node = [Xml.XmlElement]$ratings_node.AppendChild($xml.CreateElement("rating"))
    $rating_node.SetAttribute('name', 'imdb')
    $rating_node.SetAttribute('max', '10')
    $rating_node.AppendChild($xml.CreateElement("value")).InnerText = ("{0:f1}" -f $kp_info.rating.imdb).Replace(',', '.')
    $rating_node.AppendChild($xml.CreateElement("votes")).InnerText = ("{0:f0}" -f $kp_info.votes.imdb).Replace(',', '.')
  }
  
  # if ($kp_info.top250) {
  # $doc.AppendChild($xml.CreateElement("top250")).InnerText = $kp_info.top250
  # }
  
  ### Images
  # <thumb spoof="" cache="" aspect="poster" preview="">https://assets.fanart.tv/fanart/movies/18/movieposter/the-fifth-element-5cd19222eba01.jpg</thumb>
  # <thumb spoof="" cache="" aspect="landscape" preview="">https://assets.fanart.tv/fanart/movies/18/moviethumb/the-fifth-element-5cfbfe6a9fe7f.jpg</thumb>
  # <thumb spoof="" cache="" aspect="clearlogo" preview="">https://assets.fanart.tv/fanart/movies/18/hdmovielogo/the-fifth-element-505151cfaeece.png</thumb>
  # <thumb spoof="" cache="" aspect="clearart" preview="">https://assets.fanart.tv/fanart/movies/18/hdmovieclearart/the-fifth-element-54110ff055e7a.png</thumb>
  # <thumb spoof="" cache="" aspect="keyart" preview="">https://assets.fanart.tv/fanart/movies/18/movieposter/the-fifth-element-540d76d065310.jpg</thumb>
  # <thumb spoof="" cache="" aspect="discart" preview="">https://assets.fanart.tv/fanart/movies/18/moviedisc/the-fifth-element-512bfd3b590b1.png</thumb>
  # <thumb spoof="" cache="" aspect="banner" preview="">https://assets.fanart.tv/fanart/movies/18/moviebanner/the-fifth-element-535fb3fbda854.jpg</thumb>
  
  ### Poster
  $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("thumb"))
  $node.SetAttribute('aspect', 'poster')
  $node.SetAttribute('preview', $kp_info.poster.previewUrl)
  $node.InnerText = $kp_info.poster.url
  
  ### Landscape
  $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("thumb"))
  $node.SetAttribute('aspect', 'landscape')
  $node.SetAttribute('preview', $kp_info.backdrop.previewUrl)
  $node.InnerText = $kp_info.backdrop.url
  
  ### Logo
  $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("thumb"))
  $node.SetAttribute('aspect', 'clearlogo')
  # $node.SetAttribute('preview', $kp_info.logo.previewUrl)
  $node.InnerText = $kp_info.logo.url
  
  ### Для View: Media info
  # ???
  # <fanart>
  # <thumb colors="" preview="https://image.tmdb.org/t/p/w780/ABJOcPC4SFzyaRpYOvRtHKiSbX.jpg">https://image.tmdb.org/t/p/original/ABJOcPC4SFzyaRpYOvRtHKiSbX.jpg</thumb>
  # </fanart>
  $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("fanart"))
  $thumb_node = [Xml.XmlElement]$node.AppendChild($xml.CreateElement("thumb"))
  $thumb_node.SetAttribute('preview', $kp_info.backdrop.previewUrl)
  $thumb_node.InnerText = $kp_info.backdrop.url
  
  # ???
  # <art>
  # <fanart>https://image.tmdb.org/t/p/original/ABJOcPC4SFzyaRpYOvRtHKiSbX.jpg</fanart>
  # <poster>https://image.tmdb.org/t/p/original/tXl4LcgFAjDvD17ThWEabfAVNVY.jpg</poster>
  # <thumb>image://video@%2fstorage%2fCOMP19%2fVideo%2fDisk_H%2f%d0%a0%d0%be%d1%81%d1%81%d0%b8%d1%8f%2fTelekinez.2023.WEB-DL.1080p.ELEKTRI4KA.UNIONGANG.mkv/</thumb>
  # </art>
  $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("art"))
  $node.AppendChild($xml.CreateElement("fanart")).InnerText = $kp_info.backdrop.url
  $node.AppendChild($xml.CreateElement("poster")).InnerText = $kp_info.poster.url
  
  
  ### IDs
  # if ($kp_info.externalId.imdb) {
  # $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("uniqueid"))
  # $node.SetAttribute('type', 'imdb')
  # $node.InnerText = $kp_info.externalId.imdb
  # }
  
  $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("uniqueid"))
  $node.SetAttribute('type', 'kinopoisk')
  # $node.SetAttribute('default', 'true')
  $node.InnerText = $kp_info.id
  
  # <uniqueid type="imdb">tt0119116</uniqueid>
  # <uniqueid type="tmdb" default="true">18</uniqueid>
  $kp_info.externalId.psobject.Properties.GetEnumerator() | % {
    $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("uniqueid"))
    $node.SetAttribute('type', $_.Name)
    $node.InnerText = $_.Value
  }
  
  
  
  ### Genres
  # <genre>Science Fiction</genre>
  $kp_info.genres | % {
    $doc.AppendChild($xml.CreateElement("genre")).InnerText = $_.name
  }
  
  ### Countries
  # <country>France</country>
  # <country>United Kingdom</country>
  $kp_info.countries | % {
    $doc.AppendChild($xml.CreateElement("country")).InnerText = $_.name
  }
  
  
  ### Save
  $nfo_file = [io.fileinfo]$VideoFilePath
  $nfo_path = Join-Path $nfo_file.DirectoryName ($nfo_file.BaseName + ".nfo")
  $xml.Save($nfo_path)
  Write-Host "Export-KodiNfo: NFO file saved to '$nfo_path'" -fo Green
}


function Create-KodiMoviesNfo {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$Folder,
    [int]$Limit = [int]::MaxValue
  )
  
  Write-Host "Create-KodiMoviesNfo: begin"
  
  $Folder = (Resolve-Path $Folder).Path
  Write-Host "Create-KodiMoviesNfo: Folder: '$Folder'"
  
  $config = Get-Config
  $video_ext = @($config.VideoFilesExtensions | % { "*.$_" })
  
  $stat = [List[PSCustomObject]]::new()
  
  dir $Folder -Include $video_ext -Recurse | select -First $Limit | % {
    $file = $_
    Write-Host "Create-KodiMoviesNfo: process file '$($file.FullName)'"
    
    $parsed_info = $null
    $kp_info_all = @()
    $success = $false
    $message = ""
    
    [FilmInfo]$parsed_info = Parse-FileName $file.Name
    
    try {
      
      if ($parsed_info.Name) {
        
        Write-Host "Create-KodiMoviesNfo: Parsed file name:`r`n$(($parsed_info | fl * -Force | Out-String).Trim())`r`n" -fo Cyan
        
        $kp_info_all = @(Find-KinopoiskMovie -Name $parsed_info.Name)
        if ($kp_info_all) {
          Write-Host "Found movie(s) at Kinopoisk:`r`n$($kp_info_all | select id, name, alternativeName, type, year | ft -AutoSize | Out-String)" -fo Cyan
          
          $kp_info = $null
          
          ### Найден только 1 фильм:
          if ($kp_info_all.Length -eq 1) {
            $kp_info = $kp_info_all[0]
            
          } else {
            
            ### Ищем по году +-1:
            if ($parsed_info.Year) {
              $parsed_year = [int]($parsed_info.Year)
              # Write-Host "parsed_year[$($parsed_year.GetType())]: [$parsed_year]" -fo Cyan
              $years = @($parsed_year, ($parsed_year - 1), ($parsed_year + 1)) ### !!! скобки обязательно
              # Write-Host "years[$($years.GetType())]: [$years]" -fo Cyan
              foreach ($year in $years) {
                Write-Host "Find by year $year" -fo Cyan
                $delta = $year - $parsed_year
                $delta_msg = if ($delta) { " ($('{0:+#;-#;0}' -f $delta))" } else { '' }
                $kp_info_year = @($kp_info_all | ? { $_.year -eq $year })
                if ($kp_info_year) {
                  if ($kp_info_year.Length -eq 1) {
                    $message = "Found movie by year $year$delta_msg"
                    Write-Host "Create-KodiMoviesNfo: $message" -fo Green
                    $kp_info = $kp_info_year[0]
                    break
                  } else {
                    $message = "Found multiple by year $year$delta_msg, select 1st"
                    Write-Host "Create-KodiMoviesNfo: $message" -fo Green
                    $kp_info = $kp_info_year[0]
                    break
                  }
                }
              }
              
            } else {
              throw "NOT IMPLEMENTED: no year"
            }
            
          }
          
          if ($kp_info) {
            Export-KodiNfo -VideoFilePath $file.FullName -kp_info $kp_info
            $success = $true
            
          } else {
            throw "Can not find movie in Kinopoisk results"
          }
          
        } else {
          throw "Can not find movie at Kinopoisk: '$($parsed_info.Name)'"
        }
        
      } else {
        throw "Can not parse file name '$($file.Name)'"
      }
      
    } catch {
      Write-Host ("ERROR: " + ($_ | fl * -Force | Out-String).Trim()) -ForegroundColor 'Red'
      $message = $_.Exception.Message
    }
    
    $stat.Add([PSCustomObject][ordered]@{
        Success          = $success
        FilePath         = $file.FullName
        ParsedName       = $parsed_info.Name
        ParsedYear       = $parsed_info.Year
        KinopoiskFound   = $kp_info_all.Length
        KinopoiskResults = $kp_info_all
        KinopoiskId      = $kp_info.id
        Message          = $message
      }
    )
    
  } ### dir
  
  Write-Host "`r`n=== RESULTS ===" -fo Magenta
  
  $ok = @($stat | ? { $_.Success })
  if ($ok) {
    Write-Host "Processed files ($($ok.Count)):" -ForegroundColor Green
    $ok | % {
      Write-Host "`r`n==="
      Write-Host "$(($_ | select * -ExcludeProperty KinopoiskResults | fl * | Out-String).Trim())" -fo Green
      if ($_.KinopoiskResults -and $_.KinopoiskResults.Length) {
        Write-Host "Kinopoisk results:`r`n$(($_.KinopoiskResults | select id, name, alternativeName, type, year | ft -AutoSize | Out-String).Trim())" -fo Cyan
      }
    }
  }
  
  $err = @($stat | ? { !$_.Success })
  if ($err) {
    Write-Host "`r`nNot processed files ($($err.Count)):" -ForegroundColor Red
    $err | % {
      Write-Host "`r`n==="
      Write-Host "$(($_ | select * -ExcludeProperty KinopoiskResults | fl * | Out-String).Trim())" -fo red
      if ($_.KinopoiskResults -and $_.KinopoiskResults.Length) {
        Write-Host "Kinopoisk results:`r`n$(($_.KinopoiskResults | select id, name, alternativeName, type, year | ft -AutoSize | Out-String).Trim())" -fo Cyan
      }
    }
  }
  
  Write-Host "`r`n === TOTALS ==="
  Write-Host "Processed files: $($ok.Count)" -ForegroundColor Green
  Write-Host "Not processed files: $($err.Count)" -ForegroundColor Red
  
}