films.ps1
using namespace System.Collections.Generic Write-Verbose "Include script '$PSCommandPath'" ### Includes: #. "$PSScriptRoot\common.ps1" . "$PSScriptRoot\logging.ps1" . "$PSScriptRoot\text.ps1" . "$PSScriptRoot\tmdb.ps1" . "$PSScriptRoot\youtube.ps1" if (0) { . "$PSScriptRoot\kinopoisk.ps1" } ### Types: enum MediaContentType { None Movie TVShow MusicVideo } class ParsedName { [string]$FileName $Tokens [List[string]]$UnknownTokens = [List[string]]::new() [string]$Name [MediaContentType]$ContentType [int]$Year [string]$Resolution [string]$Source [string]$DynamicRange [string]$Codec [List[string]]$Sound = [List[string]]::new() [string]$Container [int]$Season [string]$SeasonSuffix [List[string]]$Comments = [List[string]]::new() } class ExportKodiNfoResult { [List[string]]$Warnings = [List[string]]::new() [List[string]]$Errors = [List[string]]::new() } class KinopoiskInfo { $Id $Search $SearchMessage } class TmdbInfo { $Id $Search $SearchMessage $Trailer } class YoutubeInfo { # $TrailerId # $TrailerUrl $Trailer } class MediaInfo { # $Item [string]$Path [string]$Name # [type]$Type [string]$BaseName [string]$Directory [MediaContentType]$ContentType [ParsedName]$ParsedName # [hashtable]$KP [KinopoiskInfo]$KP = [KinopoiskInfo]::new() [TmdbInfo]$TMDB = [TmdbInfo]::new() [YoutubeInfo]$Youtube = [YoutubeInfo]::new() } ### Variables: $app_name = 'dbprv.MultiMediaHelpers' $kodi_nfo_templates = [Dictionary[MediaContentType, string]]::new() $kodi_nfo_templates.Add('Movie', @" <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <movie> <title/> <originaltitle/> <year/> <plot/> <mpaa/> </movie> "@ ) $kodi_nfo_templates.Add('TVShow', @" <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <tvshow> <title/> <originaltitle/> <year/> <season/> <plot/> <mpaa/> <episodeguide/> </tvshow> "@ ) #<episode/> #??? проверить: tagline = shortDescription !!! ломает сканирование $script:parsed_names_hints = @{ } ### Functions: function Parse-FileName { [CmdletBinding()] [OutputType([ParsedName])] param ( [Parameter(Mandatory = $true)] [string]$Name, [MediaContentType]$ContentType = 'Movie' ) Write-Verbose "Parse-FileName: Name: '$Name'" $config = Get-Config $media_params = $config.FileNameTokens $result = [ParsedName]@{ FileName = $Name ContentType = $ContentType } $containers = @( [io.path]::GetExtension($Name).Trim('.') ) ### First - split file name to tokens: ### Заменить season N, сезон N на [SN] $prepare_name = $Name -replace '(season|сезон)\s*(\d+)', '[S$2]' $sb = [System.Text.StringBuilder]::new($prepare_name) ### Разбить на строки части в скобках: ### Части в скобках далее не разбиваются $sb.Replace('[', "`n[") >$null $sb.Replace(']', "]`n") >$null $sb.Replace('(', "`n[") >$null $sb.Replace(')', "]`n") >$null ### Поместить в скобки части, которые не надо разбивать (например H.265): $media_params.DoNotSplit | % { $sb.Replace($_, "`n[$_]`n") >$null } Write-Verbose "Parse-FileName: sb:`r`n===`r`n$sb`r`n===" $result.Tokens = @( $sb.ToString().Split("`n", [StringSplitOptions]::RemoveEmptyEntries) | % { if ($_.StartsWith('[')) { ### Строки в скобках не разбиваем: $_ } else { ### Разбиваем по пробелам: $_ -split ' ' } } | % { ### Строки в скобках не разбиваем: if ($_.StartsWith('[')) { $_ } else { ### Разбиваем по ._ $_ -split '[._]' } } | % { "$_".Trim(' -') } | ? { $_ } | % { if ($_.StartsWith('[')) { [pscustomobject]@{ Value = $_.Trim('[]') Brackets = $true } } else { [pscustomobject]@{ Value = $_ Brackets = $false } } } ) Write-Verbose "Parse-FileName: tokens:`r`n$(($result.Tokens | ft -AutoSize | Out-String).Trim())" ### Second - parse tokens: $name_done = $false $name_tokens = [List[string]]::new() for ($i = 0; $i -lt $result.Tokens.Length; $i++) { $token = $result.Tokens[$i].Value $brackets = $result.Tokens[$i].Brackets Write-Verbose "Process token '$token'" if ($token -in $media_params.Resolutions) { $result.Resolution = $token $name_done = $true } elseif ($token -in $media_params.Sources) { $result.Source = $token $name_done = $true } elseif ($token -in $media_params.DynamicRanges) { $result.DynamicRange = $token $name_done = $true } elseif ($token -in $media_params.Codecs) { $result.Codec = $token $name_done = $true } elseif ($token -in $config.VideoFilesExtensions) { $result.Container = $token $name_done = $true } elseif ($token -in $media_params.Sound) { $result.Sound.Add($token) $name_done = $true } elseif ((!$result.Year) -and ($token -match '^(1|2)\d{3}$')) { $result.Year = $token $name_done = $true } elseif (($ContentType -eq 'TVShow') -and ($token -match '^(S|season\s*|сезон\s*)(\d+)(.*)$')) { $result.Season = $Matches[2] $result.SeasonSuffix = $Matches[3] $name_done = $true } elseif ((!$name_done) -and (!$brackets)) { $name_tokens.Add($token) } else { $result.UnknownTokens.Add($token) } } $result.Name = $name_tokens -join ' ' ### Для сериалов установить по-умолчанию сезон 1 if (($ContentType -eq 'TVShow') -and (!$result.Season)) { $result.Season = 1 } return $result } function Export-KodiNfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [MediaInfo]$MediaInfo ) Write-Verbose "Export-KodiNfo: begin" $content_type = $MediaInfo.ContentType $parsed_info = $MediaInfo.ParsedName $kp_info = $MediaInfo.KP.Search $tmdb_info = $MediaInfo.TMDB.Search $tmdb_trailer = $MediaInfo.TMDB.Trailer $youtube_trailer = $media_info.Youtube.Trailer $xml = [xml]$kodi_nfo_templates[$content_type] $result = [ExportKodiNfoResult]::new() $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 # } ### 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() | % { # Write-Verbose "Export-KodiNfo: externalId: name: '$($_.Name)', value: '$($_.Value)'" if ($_.Value) { $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("uniqueid")) $node.SetAttribute('type', $_.Name) $node.InnerText = $_.Value } } ### Только для сериалов: if ($content_type -eq 'TVShow') { $doc.season = "$($parsed_info.Season)" # <episodeguide>{"imdb":"tt0804484","tmdb":"93740"}</episodeguide> # $doc.episodeguide = (ConvertTo-Json $kp_info.externalId) # ### Если TMDB ID нет, ищем сериал на TMDB # if (!$kp_info.externalId.tmdb) { # $tmdb_info = Find-TmdbTVShowSingle -Name $kp_info.name -Year $kp_info.year # if ($tmdb_info) { # Add-Member -InputObject $kp_info.externalId -MemberType NoteProperty -Name tmdb -Value $tmdb_info.id # } # } $episodeguide = @{ } $xml.tvshow.uniqueid | % { if ($_.InnerText) { $episodeguide[$_.GetAttribute('type')] = $_.InnerText } } # $kp_info.externalId.psobject.Properties.GetEnumerator() | % { # if ($_.Value) { # $episodeguide[$_.Name] = "$($_.Value)" # } # # $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("uniqueid")) # # $node.SetAttribute('type', $_.Name) # # $node.InnerText = $_.Value # } if (!$episodeguide.Count) { $warn = "No external IDs for '$($kp_info.Name)' in Kinopoisk info" $result.Warnings.Add($warn) Write-Warning "Export-KodiNfo: $warn" } $doc.episodeguide = [string](ConvertTo-Json $episodeguide -Compress) } ### 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> # ??? # <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> $art_node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("art")) ### Poster if ($kp_info.poster.url) { $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 $art_node.AppendChild($xml.CreateElement("poster")).InnerText = $kp_info.poster.url } ### Landscape if ($kp_info.backdrop.url) { $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 ### Для 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_node.AppendChild($xml.CreateElement("fanart")).InnerText = $kp_info.backdrop.url } ### Logo if ($kp_info.logo.url) { $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 } ### 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 } ### Trailers if ($tmdb_trailer) { $doc.AppendChild($xml.CreateElement("trailer")).InnerText = $tmdb_trailer.KodiUrl # $tmdb_trailers | select -First 1 | % { # $doc.AppendChild($xml.CreateElement("trailer")).InnerText = $_.KodiUrl # } } elseif ($youtube_trailer) { $doc.AppendChild($xml.CreateElement("trailer")).InnerText = $youtube_trailer.KodiUrl } ### generator <# <generator> <appname>dbprv.MultiMediaHelpers</appname> <appversion>1.0.0</appversion> <kodiversion>20</kodiversion> <datetime>2024-02-23T19:26:24Z</datetime> </generator> #> $node = [Xml.XmlElement]$doc.AppendChild($xml.CreateElement("generator")) $node.AppendChild($xml.CreateElement("appname")).InnerText = $app_name $node.AppendChild($xml.CreateElement("datetime")).InnerText = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') #o, s, u ### Save $nfo_path = '' if ($content_type -eq 'Movie') { if (Test-Path -LiteralPath $MediaInfo.Path -PathType Leaf) { ### nfo for files save to <file_name>.nfo: $nfo_path = Join-Path $MediaInfo.Directory ($MediaInfo.BaseName + ".nfo") } elseif (Test-Path -LiteralPath $MediaInfo.Path -PathType Container) { ### nfo for dirs save to movie.nfo: $nfo_path = Join-Path $MediaInfo.Directory "movie.nfo" } else { throw "Unknown item type: '$($MediaInfo.Path)'" } } elseif ($content_type -eq 'TVShow') { $nfo_path = Join-Path $MediaInfo.Directory "tvshow.nfo" } else { throw "NOT IMPLEMENTED: content type '$content_type'" } $xml.Save($nfo_path) Write-Host "Export-KodiNfo: NFO file saved to '$nfo_path'" -fo Green return $result } ### Костыль для неправильно определяющихся фильмов и сериалов ### Для фильма: прочитать имя и год из файла mmh.txt в папке с фильмами ### Для сериала: прочитать имя и год из файла mmh.txt в папке сериала ### Формат mmh.txt: имя файла/папки разделитель `t|; имя год function Get-ParsedInfoFromHintFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Folder, [string]$Name # [MediaContentType]$ContentType ) Write-Verbose "Get-ParsedInfoFromHintFile: Folder: '$Folder', Name: '$Name'" if (!$script:parsed_names_hints.Count) { $config = Get-Config $path = Join-Path $Folder $config.HintFileName Write-Verbose "Get-ParsedInfoFromHintFile: hints path: '$path'" if (Test-Path $path -PathType Leaf) { gc $path | % { "$_".Trim() } | ? { $_ } | % { $k, $v = $_ -split "[`t|;]", 2 $script:parsed_names_hints[$k] = $v } Write-Verbose "$script:parsed_names_hints:`r`n$(($script:parsed_names_hints | Out-String).Trim())" } else { Write-Verbose "Get-ParsedInfoFromHintFile: hint file not found" return } } return $script:parsed_names_hints[$Name] # if ($ContentType -eq 'Movie') { # # # } elseif ($ContentType -eq 'TVShow') { # $mmh_file_path = Join-Path $item.FullName "mmh.txt" # if (Test-Path -LiteralPath $mmh_file_path -PathType Leaf) { # Write-Verbose "Create-KodiMoviesNfo: process file mmh.txt" # $mmh_file_info = gc -LiteralPath $mmh_file_path -First 1 # [ParsedName]$parsed_name_from_mmh_file = Parse-FileName -Name $mmh_file_info -ContentType $ContentType # # # if ($parsed_name_from_mmh_file) { # if ($parsed_name_from_mmh_file.Name) { # Write-Verbose "Create-KodiMoviesNfo: set parsed name from mmh file" # $parsed_name.Name = $parsed_name_from_mmh_file.Name # } # if ($parsed_name_from_mmh_file.Year) { # Write-Verbose "Create-KodiMoviesNfo: set parsed yaer from mmh file" # $parsed_name.Year = $parsed_name_from_mmh_file.Year # } # } # } # } } ### Public function: function Create-KodiMoviesNfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Folder, [int]$Limit = [int]::MaxValue, [string[]]$CountriesAny, [MediaContentType]$ContentType, [switch]$SaveInfo # [switch]$Recurse ) Start-ScriptLogging try { Write-Host "Create-KodiMoviesNfo: begin" $Folder = (Resolve-Path $Folder).Path Write-Host "Create-KodiMoviesNfo: Folder: '$Folder'" $config = Get-Config $video_masks = @($config.VideoFilesExtensions | % { "*.$_" }) Write-Host "Create-KodiMoviesNfo: video_masks: [$video_masks]" $disc_rip_dirs = @( 'BDMV' 'VIDEO_TS' ) if ($config.DiskRipDirs) { $disc_rip_dirs = @($config.DiskRipDirs) } $season_dir_pattern = '^(S|season[\s_]*|сезон[\s_]*)(?<season>\d{1,2})$' $stat = [List[PSCustomObject]]::new() $items = @( $(if ($ContentType -eq 'Movie') { ### Get dirs - disc-rips: $dirs = @(dir $Folder -Recurse -Directory | ? { $_.EnumerateDirectories() | ? { $_.Name -in $disc_rip_dirs } }) $dirs_arr = @($dirs | % { $_.FullName + [io.path]::DirectorySeparatorChar }) ### Get files: if ($dirs_arr) { ### Get files not in disc-rips dirs: Write-Verbose "Create-KodiMoviesNfo: dirs_arr:`r`n$($dirs_arr -join "`r`n")" dir $Folder -Include $video_masks -File -Recurse -Force ` | ? { !$($f = $_; $dirs_arr | % { $f.FullName.StartsWith($_) | ? { $_ } }) } # | ? { # $f = $_ # $include = $true # foreach ($d in $dirs_arr) { # if ($f.FullName.StartsWith($d)) { # $include = $false # } # } # $include # } | select -First $Limit # !($dirs_arr | % { $f.FullName.StartsWith($_) }) $dirs } else { dir $Folder -Include $video_masks -File -Recurse -Force } } elseif ($ContentType -eq 'TVShow') { ### Только каталоги с видеофайлами: #$video_masks = @("*.mkv", "*.mp4") dir $Folder -Directory -Recurse -Force | ? { ### Если в имени будут скобки [], надо экранировать: $fn = [System.Management.Automation.WildcardPattern]::Escape($_.FullName) $video_masks | % { dir "$fn\$_" } } | % { if ($_.Name -match $season_dir_pattern) { Write-Verbose "Create-KodiMoviesNfo: Found season folder: '$($_.FullName)'" gi -LiteralPath $_.Parent.FullName } else { Write-Verbose "Create-KodiMoviesNfo: Found TVShow folder: '$($_.FullName)'" $_ } } # dir $Folder -Directory -Exclude $config.ExcludeFolders -Recurse | select -First $Limit } else { throw "NOT IMPLEMENTED: content type '$ContentType'" }) | Sort-Object FullName -Unique | ? { $_.Name -notmatch '^__' } | select -First $Limit ) Write-Verbose "All items to process: $($items.Length) $($ContentType)(s):`r`n$($items.FullName -join "`r`n")" # return $save_info_dir = Join-Path $Folder ".media_info" if ($SaveInfo) { if (!(Test-Path $save_info_dir -PathType Container)) { (mkdir $save_info_dir -ea Stop).Attributes = 'Hidden' } } $items | % { $item = $_ Write-Host "Create-KodiMoviesNfo: process item '$($item.FullName)'" $parsed_name = $null $success = $false $message = "" $kp_search = $null $tmdb_id = 0 $tmdb_search = $null $tmdb_videos = $null $export_result = $null $warnings = [List[string]]::new() $errors = [List[string]]::new() $media_info = [MediaInfo]@{ # Item = $item Name = $item.Name BaseName = $item.BaseName Path = $item.FullName # Type = $item.GetType() Directory = $(if ($item.PSIsContainer) { $item.FullName } else { $item.DirectoryName }) ContentType = $ContentType } # Write-Host ("`r`n=== media_info:`r`n" + ($media_info | fl * -Force | Out-String).Trim()) -ForegroundColor 'Cyan' [ParsedName]$parsed_name = Parse-FileName -Name $item.Name -ContentType $ContentType $media_info.ParsedName = $parsed_name ### Костыль для неправильно определяющихся фильмов и сериалов ### Для фильма: прочитать имя и год из файла mmh.txt в папке с фильмами ### Для сериала: прочитать имя и год из файла mmh.txt в папке сериала $hint_result = Get-ParsedInfoFromHintFile -Folder $Folder -Name $item.Name if ($hint_result) { [ParsedName]$parsed_name_from_hint_file = Parse-FileName -Name $hint_result -ContentType $ContentType if ($parsed_name_from_hint_file) { if ($parsed_name_from_hint_file.Name) { Write-Verbose "Create-KodiMoviesNfo: set parsed name from hint file" $parsed_name.Name = $parsed_name_from_hint_file.Name $parsed_name.Comments.Add("Name from hint file") } ### Иногда на Кинопоиске попадаются фильмы/сериалы без года # if ($parsed_name_from_hint_file.Year) { Write-Verbose "Create-KodiMoviesNfo: set parsed yaer from hint file" $parsed_name.Year = $parsed_name_from_hint_file.Year $parsed_name.Comments.Add("Year from hint file") # } } } # !!! -EnumsAsStrings - no in PS5 Write-Verbose ("`r`n=== media_info:`r`n" + ($media_info | ConvertTo-Json -Depth 5 | Out-String).Trim()) try { if ($parsed_name.Name) { Write-Host "Create-KodiMoviesNfo: Parsed item name:`r`n$(($parsed_name | fl * -Force | Out-String).Trim())`r`n" -fo Cyan $kp_search = Find-KinopoiskMovieSingle -Name $parsed_name.Name ` -Year $parsed_name.Year ` -CountriesAny $CountriesAny ` -Type $ContentType ` -TryTranslitName $media_info.KP.Search = $kp_search.Result $media_info.KP.SearchMessage = $kp_search.Message if ($kp_search.Success) { $tmdb_id = $kp_search.Result.externalId.tmdb $kp_year = $media_info.KP.Search.year ### Если TMDB ID нет в результате Кинопоиска, ищем на TMDB, добавляем ID ### TMDB ID необходим для инфы об эпизодах сериалов и для трейлеров if (!$tmdb_id) { $tmdb_search = $null $imdb_id = $kp_search.Result.externalId.imdb ### Перебираем все имена из результата Кинопоиска $names = [List[string]]::new() $kp_search.Result.name, $kp_search.Result.alternativeName, $kp_search.Result.enName ` | % { "$_".Trim() } | ? { $_ } | % { $names.Add($_) } $kp_search.Result.names.name | % { "$_".Trim() } | ? { $_ } | ? { $_ -notin $names } | % { $names.Add($_) } $kp_search.Result.internalNames | % { "$_".Trim() } | ? { $_ } | ? { $_ -notin $names } | % { $names.Add($_) } if (!$names) { throw "Empty names list for TMDB search" } Write-Verbose "Create-KodiMoviesNfo: search TMDB by names($($names.Count)): [`r`n$($names -join "`r`n")`r`n]" $params = @{ ImdbId = $imdb_id ContentType = $ContentType OriginalName = $kp_search.Result.alternativeName Year = $kp_search.Result.year OriginalLanguage = $kp_search.Result.OriginalLanguage } foreach ($n in $names) { Write-Verbose "Create-KodiMoviesNfo: search TMDB by name '$n'" ### Года Кинопоиска и TMDB могут не совпадать (например "Иные") $params.Name = $n $tmdb_search = Find-TmdbSingle @params -ErrorAction Continue if ($tmdb_search.Success -and $tmdb_search.Result.id) { Write-Host "TMDB result:`r`n$($tmdb_search.Result | select id, title, name, original_title, original_name, original_language, release_date, year | ft -AutoSize | Out-String)" -fo Cyan $tmdb_id = $tmdb_search.Result.id $media_info.TMDB.Search = $tmdb_search.Result if ($kp_search.Result.externalId -eq $null) { $kp_search.Result.externalId = [PSCustomObject]@{ } } Add-Member -InputObject $kp_search.Result.externalId -MemberType NoteProperty -Name tmdb -Value $tmdb_id -Force break } else { $warnings.Add("TMDB search: $($tmdb_search.Message)") } } } else { $tmdb_search = [FindTmdbResult]@{ # Name = $Name # Year = $Year # CountriesAny = $CountriesAny Type = $ContentType Success = $true Message = "TMDB ID in Kinopoisk result" } } $youtube_region = $config.Youtube.Region ### Add trailer if ($tmdb_id) { $media_info.TMDB.Id = $tmdb_id if ($tmdb_search) { $media_info.TMDB.SearchMessage = $tmdb_search.Message } $tmdb_trailers = @(Get-TmdbTrailers -Id $tmdb_id -ContentType $ContentType | ? { $_.YoutubeId }) ### Валидация трейлера: иногда трейлер есть на TMDB, но удален, закрыт и т.д. $cnt = 0 foreach ($trailer in $tmdb_trailers) { $cnt++ $youtube_video = Get-YoutubeVideo -Id $trailer.YoutubeId if ($youtube_video) { ### Проверить, что трейлер не заблокирован в текущем регионе: $blocked_regions = @($youtube_video.contentDetails.regionRestriction.blocked) if ($blocked_regions -and ($blocked_regions -contains $youtube_region)) { Write-Verbose "Create-KodiMoviesNfo: TMDB trailer #$($cnt) blocked in region $youtube_region, skip: '$($trailer.YoutubeId)'" continue } ### Проверить только разрешенные регионы: $allowed_regions = @($youtube_video.contentDetails.regionRestriction.allowed) if ($allowed_regions -and ($allowed_regions -notcontains $youtube_region)) { Write-Verbose "Create-KodiMoviesNfo: TMDB trailer #$($cnt) not allowed in region $youtube_region, skip: '$($trailer.YoutubeId)'" continue } Write-Verbose "Create-KodiMoviesNfo: select TMDB trailer #$($cnt): '$($trailer.YoutubeId)'" $media_info.TMDB.Trailer = $trailer break } Write-Verbose "Create-KodiMoviesNfo: invalid TMDB trailer #$($cnt): '$($trailer.YoutubeId)'" } } else { Write-Warning "Create-KodiMoviesNfo: not found in TMDB" } ### Если трейлер на TMDB не найден, ищем на Youtube: if ((!$media_info.TMDB.Trailer) -and $config.Youtube) { $media_info.Youtube.Trailer = Find-YoutubeTrailer -Name $parsed_name.Name -ContentType $ContentType } ### Экспорт всей найденной инфы в Kodi .nfo: $export_result = Export-KodiNfo -MediaInfo $media_info $success = $true } else { throw "Can not find movie at Kinopoisk: '$($parsed_name.Name)'" # throw "Can not find movie in Kinopoisk results" } } else { throw "Can not parse item name '$($item.Name)'" } } catch { Write-Host ("ERROR: " + ($_ | fl * -Force | Out-String).Trim()) -ForegroundColor 'Red' $message = $_.Exception.Message } $season_str = if ($parsed_name.Season) { " / " + ("S{0:d2}" -f $parsed_name.Season) } else { '' } $stat.Add([PSCustomObject][ordered]@{ Success = $success ItemPath = $item.FullName ItemName = Split-Path $item.FullName -Leaf ### Parsed info: ParsedName = $parsed_name.Name NameTranslit = $kp_search.NameTranslit ParsedYear = $parsed_name.Year # KinopoiskFound = $kp_find_result.AllResults.Length ### Summary info: KinopoiskResultStr = "$($kp_search.Result.name) / $($kp_search.Result.alternativeName) / $($kp_search.Result.year)$($season_str)" TmdbResultStr = "$($tmdb_search.Result.name) / $($tmdb_search.Result.original_name) / $($tmdb_search.Result.year)$($season_str)" ### Kinopoisk: KinopoiskId = $kp_search.Result.id KinopoiskResult = $kp_search.Result KinopoiskAllResults = $kp_search.AllResults KinopoiskMessage = $kp_search.Message ### TMDB: TmdbId = $tmdb_id TmdbMessage = $tmdb_search.Message TmdbTrailer = $media_info.TMDB.Trailer.KodiUrl YoutubeTrailer = $media_info.Youtube.Trailer.KodiUrl Warnings = $warnings Errors = $errors ExportWarnings = $export_result.Warnings ExportErrors = $export_result.Errors } ) if ($SaveInfo) { Out-File -InputObject (ConvertTo-Json $media_info -Depth 5) ` -LiteralPath (Join-Path $save_info_dir "$($media_info.BaseName).json") ` -enc utf8 -Force } } ### item Write-Host "`r`n=== RESULTS ===" -fo Magenta $ok = @($stat | ? { $_.Success }) if ($ok) { Write-Host "Processed items ($($ok.Count)):" -ForegroundColor Green $ok | % { Write-Host "`r`n===" Write-Host "$(($_ | select * -ExcludeProperty KinopoiskResult, KinopoiskAllResults | fl * | Out-String).Trim())" -fo Green # if ($_.KinopoiskAllResults -and $_.KinopoiskResults.Length) { Write-Host "`r`nKinopoisk all results:`r`n$(($_.KinopoiskAllResults ` | select order, id, name, alternativeName, type, year, @{ Name = "CountriesAll"; Expression = { $_.countries.name -join ',' } } ` | ft -AutoSize | Out-String).TrimEnd())" -fo Cyan # } } Write-Host "`r`nShort list:" -fo Green Write-Host "$(($ok | select ParsedName, NameTranslit, ItemName, KinopoiskResultStr, TmdbResultStr | ft -auto | Out-String).TrimEnd())" -fo Green # KinopoiskMessage } $warn = @($stat | ? { $_.Warnings -or $_.ExportResult.Warnings.Count }) if ($warn) { Write-Host "`r`n=== WARNINGS ===" -fo Yellow $warn | % { Write-Host "`r`nItemPath: $($_.ItemPath)" -fo Yellow if ($_.Warnings) { Write-Host "Process warnings:`r`n$($_.Warnings -join "`r`n")" -fo Yellow } if ($_.ExportResult.Warnings) { Write-Host "Export warnings:`r`n$($_.ExportResult.Warnings -join "`r`n")" -fo Yellow } } # Write-Host "$(($_ | select * -ExcludeProperty KinopoiskResult, KinopoiskAllResults | fl * | Out-String).Trim())" -fo Yellow } else { Write-Host "`r`nNo warnings" -fo Green } $err = @($stat | ? { !$_.Success }) if ($err) { Write-Host "`r`nNot processed items ($($err.Count)):" -ForegroundColor Red $err | % { Write-Host "`r`n===" Write-Host "$(($_ | select * -ExcludeProperty KinopoiskResult, KinopoiskAllResults | 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`nShort list:" -fo Red Write-Host "$($err | select ParsedName, NameTranslit, ItemName, KinopoiskMessage | ft -auto | Out-String)" -fo red } Write-Host "`r`n=== TOTALS ===" Write-Host "Total items : $($stat.Count)" -fo White Write-Host "Processed : $($ok.Count)" -ForegroundColor Green Write-Host "With warnings : $($warn.Count)" -ForegroundColor Yellow Write-Host "Not processed : $($err.Count)" -ForegroundColor Red } finally { Show-NetworkStat Stop-ScriptLogging if ($SaveInfo) { Copy-ScriptLog -DestiantionDir "$save_info_dir\logs" -ErrorAction Continue } } } function Get-KodiNfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Folder, [int]$Limit = [int]::MaxValue ) process { dir $Folder -Include @("*.nfo") -Recurse -File | select -First $Limit | % { $file = $_ Write-Verbose "Get-TVShowsKodiNfo: process file '$($file.FullName)'" $xml = [xml](gc -LiteralPath $file.FullName -Raw -ErrorAction 'Stop') $root = $xml.DocumentElement $ht = [ordered]@{ Folder = $Folder Subfolder = if ($file.DirectoryName -ne $Folder) { $file.DirectoryName.Substring($Folder.Length + 1) } else { '' } # $file.DirectoryName # Dir = $file.Directory.FullName.Substring($Folder.Length + 1) FileName = $file.Name # FilePath = $file.FullName # FileRelPath = $file.FullName.Substring($Folder.Length + 1) # DirName = $file.Directory.Name Name = $root.title OriginalName = $root.originaltitle Year = $root.year TVShowSeason = $root.season HasTrailer = [bool]$root.trailer } $xml.DocumentElement.ratings.rating | % { $ht["rating_$($_.name)"] = "$($_.value)".Replace('.', ',') $ht["votes_$($_.name)"] = $_.votes # $ht["rating_$($_.GetAttribute('name'))"] = "$($_.value.innerText) ($($_.votes.innerText))" } $xml.DocumentElement.uniqueid | % { $ht["id_$($_.GetAttribute('type'))"] = $_.innerText } $ht.YoutubeTrailer = if ($root.trailer) { $youtube_id = ($root.trailer -split '=')[-1] if ($youtube_id) { "https://www.youtube.com/watch?v=$youtube_id" } } else { '' } [PSCustomObject]$ht } } } function Check-KodiNfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Folder, [int]$Limit = [int]::MaxValue ) Write-Host "`r`n=== Check Kodi Nfo in '$folder' ===" $result = Get-KodiNfo -Folder $folder -Limit $limit Write-Host "All Nfo:`r`n$(($result | select * -ExcludeProperty FilePath | ft -AutoSize | Out-String).Trim())" -fo Cyan $no_trailer = @($result | ? { !$_.HasTrailer }) if ($no_trailer) { Write-Host "`r`nNo trailer:`r`n$(($no_trailer | select Folder, Subfolder, FileName, title, originaltitle, year, tvshow_season, id_kinopoisk, id_tmdb, id_imdb | ft -AutoSize | Out-String).Trim())" -fo yellow } $no_tmdb_id = @($result | ? { !$_.id_tmdb }) if ($no_tmdb_id) { Write-Host "`r`nNo TMDB ID:`r`n$(($no_tmdb_id | select Folder, Subfolder, FileName, title, originaltitle, year, tvshow_season, id_kinopoisk, id_tmdb, id_imdb | ft -AutoSize | Out-String).Trim())" -fo red # Write-Host "`r`nNo TMDB ID:`r`n$(($no_tmdb_id | select * -ExcludeProperty FilePath | ft -AutoSize | Out-String).Trim())" -fo red } } function Export-KodiNfoCsv { [CmdletBinding(DefaultParameterSetName = 'Dir')] param ( [string[]]$Folders, [string]$ResultPath ) Write-Verbose "Export-KodiNfoCsv: begin" Write-Verbose "Export-KodiNfoCsv: Folders($($Folders.Length)):`r`n$($Folders -join "`r`n")" Write-Verbose "Export-KodiNfoCsv: ResultPath: '$ResultPath'" #Folder Subfolder FileName Name OriginalName Year TVShowSeason HasTrailer rating_kinopoisk votes_kinopoisk rating_imdb votes_imdb id_kinopoisk id_imdb id_tmdb id_kpHD YoutubeTrailer $Folders | Get-KodiNfo | Sort-Object rating_kinopoisk, rating_imdb -Descending ` | select Name, OriginalName, Year, rating_kinopoisk, rating_imdb, votes_kinopoisk, votes_imdb, Folder, Subfolder, FileName, TVShowSeason, id_kinopoisk, id_imdb, id_tmdb, id_kpHD, HasTrailer, YoutubeTrailer ` | Export-Csv $ResultPath -Delimiter ";" -NoTypeInformation -Force -Encoding "UTF8" Write-Verbose "Export-KodiNfoCsv: end" } |