Amatsukazeを利用してCM抜きMP4を自動生成する

録画してできたM2TSは1時間番組でもファイルサイズが7GBを超えるため保存やコピーに手間がかかります。そこで、MP4へ変換してファイルサイズを落として扱いやすくする必要があります。

スポーツ番組や旅番組など画像の変化の激しいものや古い映画などのようにノイズの多いものはファイルサイズは半分程度までしか落ちないこともありますが、アニメなどの変化の少ないものは10分の1程度まで落とせます。概ね2-3割程度まで落とせると考えればいいでしょう。

また、変換の際にCMを抜くとさらにファイルサイズを落とせます。通常2割弱程度がCMですので、圧縮後のファイルからさらに2割サイズを落とすことができます。ただし、手動でCMカットはかなりの手間ですので、WindowsアプリのAmatsukazeを利用します。

Amatsukazeはコマンドラインからも実行できるのでバッチファイルによる自動処理が可能です。ただし、バッチファイルは制限が多く複雑な自動処理には向きません。また、変換だけでなく、変換したファイルをNASへコピーしたり、途中で停止したり処理の確認をしたりと、様々な追加要望が出てきます。

そこで、バッチファイルからPowershellを起動する形で処理を自動化します。Powershellでは起動オプションによって動作を変えるように実装し、バッチファイル側で機能を選択する形で様々なパターンに対応できるようにしました。PowershellはWindowsのデフォルトシェルなので、Pythonのように別途ソフトをインストールすることなく自動化処理が可能になります。

Amatsukazeを利用してmp4への変換を行うPowershellスクリプトとスクリプト起動用のバッチファイルを以下に示します。具体的には以下の処理を順番に実行します。

  1. M2TSファイルをWindowsのローカルストレージにコピーする
  2. Amatsukazeを使ってCM抜き処理とmp4変換
  3. mp4とass(字幕ファイル)のNASへのバックアップ
  4. リモート視聴用ファイルの作成と再生用サーバーへのコピー
  5. PCシャットダウン

ファイルの保存位置など環境に依存するパラメータについては、スクリプトの先頭にまとめていますので、こちらを編集してください。

Amatsukazeはアプリ単体で動作するように事前に設定してください。変換パラメータについてはスクリプト側で指定するので、特に調整する必要はありません。

なお、このスクリプトはNvidiaのGPUの使用する前提で組んでいます。CPUのみでのエンコードには対応していないので、対応させたい場合はAmatsukazeのオプションパラメータを調整してください。NVEncC64.exeについては各環境に合わせてパスを変更してください。

このスクリプトは下記を考慮しています。

  • ファイルの最終書き込み日時と作成日時を比較して、変換・コピー処理が不要な場合はスキップする
  • 録画中のM2TSはバックアップしない
  • 番宣などの再生時間が極端に短いファイルはNASへのコピーはしない
  • ファイルコピー途中に強制終了した場合に次回実行時に再度コピーを行う
  • 変換途中に強制終了させた場合は次回実行時に再度変換処理を行う
  • ファイル名の先頭に’_’をつけることで、変換・コピーの対象から外す
  • 撮り貯め対象外フォルダの一部フォルダからのコピー処理をサポートする
  • ローカルディスクとNAS上のファイルをミラーリングする
  • mp4ファイルそのものに字幕データを保持する

ファイルは必ずBOM付きUTF-8で保存してください。BOMなしの場合、PowerShell内の日本語がShift JISとして認識されてしまい、PowerShellの処理文字コードであるUTF-8と食い違いが発生して正しく処理されません。

参考までに、4070Tiを使用している場合でCM抜きとmp4へのエンコードにかかる時間は、CM付きの2時間映画で12分~13分程度です。

@echo off

@REM Initialize batch
SET CURRENT_DIR_COPY_ENCODE_BACKUP_SHUTDOWN_BAT=%~dp0
cd /d %CURRENT_DIR_COPY_ENCODE_BACKUP_SHUTDOWN_BAT%
SET BATCH_NAME_COPY_ENCODE_BACKUP_SHUTDOWN_BAT=%~n0%~x0

@REM Get date/time string
for /f %%a in ('wmic os get LocalDateTime ^| findstr \.') DO SET LDT=%%a
SET CUR_DATE=%LDT:~0,8%
SET CUR_TIME=%LDT:~8,6%

@REM Constant
SET POWERSHELL_SCRIPT_PATH=%CURRENT_DIR_COPY_ENCODE_BACKUP_SHUTDOWN_BAT%\Copy_Encode_Backup_Shutdown.ps1
SET LOG_FILE_PATH_PATH=%CURRENT_DIR_COPY_ENCODE_BACKUP_SHUTDOWN_BAT%\%~n0_%CUR_DATE%_%CUR_TIME%.log

SET POWERSHELL_SCRIPT_ARGUMENT_LIST=^
-backup_m2ts ^
-backup_m2ts_within_day 0 ^
-convert_to_mp4 ^
-copy_to_nas ^
-generate_remoteplay ^
-remoteplay_synology ^
-log_file "%LOG_FILE_PATH_PATH%"

if "%1"=="" (
    @echo [%BATCH_NAME_COPY_ENCODE_BACKUP_SHUTDOWN_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT_PATH% -ArgumentList %POWERSHELL_SCRIPT_ARGUMENT_LIST%
    call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT_PATH% -ArgumentList %POWERSHELL_SCRIPT_ARGUMENT_LIST%
) else (
    @echo [%BATCH_NAME_COPY_ENCODE_BACKUP_SHUTDOWN_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT_PATH% -ArgumentList %POWERSHELL_SCRIPT_ARGUMENT_LIST%
    call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT_PATH% -ArgumentList %POWERSHELL_SCRIPT_ARGUMENT_LIST%
)

pause
Param(
    [switch]$backup_m2ts,
    [int]$backup_m2ts_within_day,
    [switch]$convert_to_mp4,
    [switch]$copy_to_nas,
    [switch]$generate_remoteplay,
    [switch]$remoteplay_rename,
    [string]$amatsukaze_directory,
    [string]$work_directory,    
    [string]$log_file,
    [switch]$quiet,
    [switch]$delete,
    [switch]$shutdown,
    [switch]$debug,
    [switch]$help
)

#########################
# Parameters
#########################
[string]$script:script_path = Split-Path -Parent $PSCommandPath
[string]$script:script_name = Split-Path -Leaf $PSCommandPath

[string]$script:log_file_path=$log_file
if([string]::IsNullOrEmpty($script:log_file_path)){
    $script:log_file_path = $PSCommandPath.Replace(".ps1",((Get-Date).ToString("_yyyyMMdd_HHmmss"))+".log")
}

[string[]]$script:source_directory_root        = @()
          $script:source_directory_root       += "\\192.168.1.7\recorded_files\keep"
          $script:source_directory_root       += "\\192.168.1.7\recorded_files\on_hold"
        #   $script:source_directory_root       += "\\192.168.1.6\tv-recorder\keep"
        #   $script:source_directory_root       += "\\192.168.1.6\tv-recorder\on_hold"

[string[]]$script:tentative_directory_root     = @()
          $script:tentative_directory_root    += "\\192.168.1.7\recorded_files\delete\7_映画(午後のロードショー)"
          $script:tentative_directory_root    += "\\192.168.1.6\tv-recorder\delete\7_映画(午後のロードショー)"

[string]$script:backup_directory_root          = "Z:\tv-recorder\m2ts"
[string]$script:convert_directory_root         = "Z:\tv-recorder\mp4"
[string]$script:nas_directory_root             = "\\192.168.1.7\converted_files\mp4"
[string]$script:remote_play_directory_root     = "Z:\tv-recorder\remote_play"
[string]$script:remote_play_directory_nas_root = "\\192.168.1.7\converted_files\remote_play"

[bool]$script:quiet_flag=$quiet

if($debug){
    [string[]]$script:source_directory_root          = @( "Z:\tv-recorder_test\recorded" )
    [string]  $script:backup_directory_root          = "Z:\tv-recorder_test\m2ts"
    [string]  $script:convert_directory_root         = "Z:\tv-recorder_test\mp4"
    [string]  $script:nas_directory_root             = "Z:\tv-recorder_test\nas"
    [string]  $script:remote_play_directory_root     = "Z:\tv-recorder_test\remote_play"
    [string]  $script:remote_play_directory_nas_root = "\\bluesky-nas\video\TV"

    if( $backup_m2ts )
    {
        foreach( $source_directory in ${script:source_directory_root} )
        {
            if( -Not(Test-Path "${source_directory}") ){
                New-Item -Type Directory ${source_directory} > $null
            }
        }
    }

    if( $backup_m2ts -and -Not(Test-Path "${script:backup_directory_root}") ){
        New-Item -Type Directory ${script:backup_directory_root} > $null
    }

    if( $convert_to_mp4 -and -Not(Test-Path "${script:convert_directory_root}") ){
        New-Item -Type Directory ${script:convert_directory_root} > $null
    }

    if( $copy_to_nas -and -Not(Test-Path "${script:nas_directory_root}") ){
        New-Item -Type Directory ${script:nas_directory_root} > $null
    }

    if( $generate_remoteplay -and -Not(Test-Path "${script:remote_play_directory_root}") ){
        New-Item -Type Directory ${script:remote_play_directory_root} > $null
    }

    if( $generate_remoteplay -and -Not(Test-Path "${script:remote_play_directory_nas_root}") ){
        New-Item -Type Directory ${script:remote_play_directory_nas_root} > $null
    }
}

[string]$script:amatsukaze_directory = $amatsukaze_directory
if( [string]::IsNullOrEmpty($script:amatsukaze_directory) ){
    $script:amatsukaze_directory = "$script:script_path"
}
[string]$script:amatsukaze_cli_exe = "$script:amatsukaze_directory\exe_files\AmatsukazeCLI.exe"
[string]$script:nvencc64_exe = "$script:amatsukaze_directory\exe_files\NVEncC\NVEncC64.exe"
[string]$script:work_directory = $work_directory
if( [string]::IsNullOrEmpty($script:work_directory) ){
    $script:work_directory = "$script:amatsukaze_directory\temp"
}
[string]$script:convert_to_mp4_commandline  = " -s 1024" `
                                            + " --drcs `"$script:amatsukaze_directory\drcs\drcs_map.txt`" "`
                                            + " -w `"$script:work_directory`" "`
                                            + " --chapter-exe `"$script:amatsukaze_directory\exe_files\chapter_exe.exe`" "`
                                            + " --jls `"$script:amatsukaze_directory\exe_files\join_logo_scp.exe`" "`
                                            + " --cmoutmask 2 "`
                                            + " -et NVEnc "`
                                            + " -e `"$script:amatsukaze_directory\exe_files\NVEncC\NVEncC64.exe`" "`
                                            + " -j `"$script:work_directory\2-enc.json`" "`
                                            + " --mp4box `"$script:amatsukaze_directory\exe_files\mp4box.exe`" "`
                                            + " -t `"$script:amatsukaze_directory\exe_files\timelineeditor.exe`" "`
                                            + " -eo `"-c hevc --profile main10 --vbrhq 0 --vbr-quality 25 --gop-len 90 --cqp 20:23:25`" "`
                                            + " -fmt mp4 "`
                                            + " -m `"$script:amatsukaze_directory\exe_files\muxer.exe`" "`
                                            + " -bcm 0.5 "`
                                            + " --chapter "`
                                            + " -f `"$script:amatsukaze_directory\avscache\17E37FEB.avs`" "`
                                            + " --subtitles "`
                                            + " --jls-cmd `"$script:amatsukaze_directory\JL\JL_Standard.txt`" "`
                                            + " --mpeg2decoder CUVID "`
                                            + " --h264decoder CUVID "`
                                            + " --ignore-no-drcsmap "`
                                            + " --no-delogo "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID171-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1032-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1040-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1048-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1056-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1064-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1072-1.lgd`" "`
                                            + " --logo `"$script:amatsukaze_directory\logo\SID1072-2.lgd`" "`

[string]$script:remoteplay_commandline_nvenc= " -c h264 "`
                                            + " --avhw "`
                                            + " --profile main10 "`
                                            + " --vbrhq 0 "`
                                            + " --vbr-quality 25 "`
                                            + " --gop-len 90 "`
                                            + " --cqp 28:30:32 "`
                                            + " --max-bitrate 1000 "`
                                            + " --output-res 1280x720 "`
                                            + " --chapter-copy "`
                                            + " --audio-copy 1 "`
                                            + " --sub-copy "`

[int]$delay_shutdown = 180
[int]$recording_detection_duration = 1*60         #Unit:sec
[int]$copy_allow_size              = 30*1000*1000 #Unit:byte
[int]$copy_allow_size_remote_play  = 5*1000*1000  #Unit:byte

#########################
# Help
#########################
if( $help ) {
    Write-Host "NAME"
    Write-Host ""
    Write-Host "   $script:script_name - M2TSファイルをCMを抜いたMP4形式に変換しNASへコピーする"
    Write-Host ""
    Write-Host "SYPNOSYS"
    Write-Host ""
    Write-Host "   $script:script_name [-amatsukaze_directory <directory path>]"
    Write-Host "                       [-backup_m2ts] [-convert_to_mp4] [-copy_to_nas] [-generate_remoteplay] [-shutdown] [-strict]"
    Write-Host "                       [-work_directory <directory path>] [-log_file <file path>] "
    Write-Host ""
    Write-Host "DESCRIPTION"
    Write-Host ""
    Write-Host "   -amatsukaze_directory"
    Write-Host "        Amatsukazeがインストールされているディレクトリを指定する。指定がない場合はデフォルトパスが設定される "
    Write-Host "        デフォルトパス:${script:script_path}"
    Write-Host ""
    Write-Host "   -work_directory"
    Write-Host "        Amatsukazeの一時ファイルの保存ディレクトリを指定する(高速なSSDを推奨)。指定がない場合はデフォルトパスが設定される "
    Write-Host "        デフォルトパス:${script:script_path}\temp"
    Write-Host ""
    Write-Host "   -backup_m2ts"
    Write-Host "        M2TSファイルのバックアップコピーを実行する。"
    Write-Host "        コピー元:${script:source_directory_root}"
    Write-Host "        コピー先:${script:backup_directory_root}"
    Write-Host ""
    Write-Host "   -backup_m2ts_within_day <days>"
    Write-Host "        M2TSファイルをコピーする際に、ここで指定された日数を経過したものはバックアップから除外する。0の場合はすべてコピーする"
    Write-Host ""
    Write-Host "   -convert_to_mp4"
    Write-Host "        M2TSファイルのバックアップコピー元にあるファイルからCMを抜いてMP4へ変換する "
    Write-Host "        コピー元:${script:backup_directory_root}"
    Write-Host "        コピー先:${script:convert_directory_root}"
    Write-Host ""
    Write-Host "   -copy_to_nas"
    Write-Host "        変換したMP4ファイルをNASへコピーする"
    Write-Host "        コピー元:${script:convert_directory_root}"
    Write-Host "        コピー先:${script:nas_directory_root}"
    Write-Host ""
    Write-Host "   -generate_remoteplay"
    Write-Host "        変換したMP4ファイルをRemotePlay用にサイズを縮小して変換しNASへコピーする "
    Write-Host "        コピー元:${script:convert_directory_root}"
    Write-Host "        変換先 :${script:remote_play_directory_root}"
    Write-Host "        コピー先:${script:remote_play_directory_nas_root}"
    Write-Host ""
    Write-Host "   -remoteplay_rename"
    Write-Host "        RemotePlay用に録画日をファイル名の先頭から後ろに移動酢する[<ファイル名>(YYYYMMDD)] "
    Write-Host "        ※m2tsのファイル名は'YYYYMMDD <ファイル名>'である必要がある"
    Write-Host ""
    Write-Host "   -quiet"
    Write-Host "        処理開始前の確認をスキップします"
    Write-Host ""
    Write-Host "   -delete"
    Write-Host "        PC上にないファイルをNASから削除します。quietオプションは無視されます"
    Write-Host ""
    Write-Host "   -shutdown"
    Write-Host "        変換終了後、${delay_shutdown}秒後にPCをシャットダウンする"
    Write-Host ""
    exit
}

###################
# Enumuration
###################
enum OutputLogType {
    Info
    Warning
    Error
    Debug
    NoType
}

###################
# Class
###################

###################
# Function
###################
function OutputLog {
    Param(
        [OutputLogType]$type = [OutputLogType]::NoType,
        [string]$function = $null,
        [string]$message        
    )

    [string]$output = ""

    if([string]::IsNullOrEmpty($function)){
        $output += [string]::Format("[${script:script_name}] " )
    } else {
        $output += [string]::Format("[${script:script_name}:${function}] " )
    }

    $output += [string]::Format("{0} : ", ((Get-Date).ToString("yyyy/MM/dd HH:mm:ss")) )

    if( $type -ne [OutputLogType]::NoType ){
        $output += [string]::Format("{0} : ", $type )
    }

    $output += [string]::Format("{0} ", $message )

    if([string]::IsNullOrEmpty($script:log_file_path)){
        Write-Output $output
    } else {
        Write-Output $output | Tee-Object -Append -FilePath $script:log_file_path
    }
}

function ConvertPath([string]$filepath){
    
    [string]$directory = ( Split-Path -Parent $filepath ) 
    [string]$filename = ( Split-Path -Leaf $filepath ) 

    [string]$modified_filename = $filename.Replace('[','`[').Replace(']','`]')

    return $directory+"\" + $modified_filename + $filename_extention
}

function GetDurationString([TimeSpan]$duration){
    [string]$duration_string=""

    if($duration.TotalSeconds -lt 60){
        $duration_string=($duration.TotalMilliseconds/1000).ToString("0.00")+" sec."
    } else {
        $duration_string+=($duration.TotalSeconds.ToString("0")/60).ToString("0")+" min. "
        $duration_string+=($duration.TotalSeconds.ToString("0")%60).ToString("0")+" sec. "
    }

    return $duration_string
}

function BackupM2ts {
    Param(
        [string[]]$sources,
        [string[]]$tentatives,
        [string]$destination
    )

    [string]$output_message = "Start backup"
    foreach( $source in $sources )
    {
        $output_message += "`n  - ${source}"
    }

    OutputLog -type Info -function "BackupM2ts" -message $output_message
        
    [string[]]$copy_files = @()
    [string[]]$destination_files = @()
    foreach( $source in $sources )
    {
        $m2ts_files = Get-ChildItem "$source" -Recurse -Filter *.m2ts -Exclude "lost+found"

        foreach( $m2ts_file in $m2ts_files ){

            if( !$m2ts_file.Name.StartsWith("_") ){

                [bool]$copy_flag = $false

                $destination_file_path = ConvertPath($m2ts_file.FullName.Replace($source,$destination))
        
                if(-Not(Test-Path $destination_file_path) ) {
                    $copy_flag = $true
                } elseif( ( (Get-Item $destination_file_path).Length -gt $m2ts_file.Length ) ){
                    $copy_flag = $true
                }

                if($copy_flag)
                {
                    [string]$temp_file_path = (Split-Path -Parent ${destination_file_path}) + "\TEMP_" +(Split-Path -Leaf ${destination_file_path})
                    if( Test-Path( $temp_file_path) ){
                        Remove-Item -Path ${temp_file_path}
                    }

                    [timespan]$time_diff = [DateTime]::Now - $m2ts_file.LastWriteTime
                    if( $time_diff.TotalSeconds -lt $recording_detection_duration ){
                        $copy_flag = $false
                    } elseif ( (${backup_m2ts_within_day} -gt 0 ) -and ( $time_diff.TotalDays -ge $backup_m2ts_within_day ) ){
                        $copy_flag = $false
                    }
                    else 
                    {
                        $find_flag = $false
                        foreach( $destination_file in $destination_files )
                        {
                            if( ( Split-Path -Leaf $m2ts_file ).Equals( ( Split-Path -Leaf $destination_file ) ) )
                            {
                                $find_flag = $true
                                break
                            }
                        }

                        if( !$find_flag )
                        {
                            $copy_files += $m2ts_file.FullName
                            $destination_files += $m2ts_file.FullName.Replace($source,$destination)
                        }
                    }
                } 
            }
        }
    }

    foreach( $tentative in $tentatives )
    {
        $m2ts_files = Get-ChildItem "$tentative" -Recurse -Filter *.m2ts -Exclude "lost+found"

        $tentative_parent = (Split-Path -Parent $tentative)

        foreach( $m2ts_file in $m2ts_files ){

            if( !$m2ts_file.Name.StartsWith("_") ){

                [bool]$copy_flag = $false

                $destination_file_path = ConvertPath($m2ts_file.FullName.Replace($tentative_parent,$destination))
        
                if(-Not(Test-Path $destination_file_path) )
                {
                    $copy_flag = $true
                } elseif( (Get-Item $destination_file_path).Length -ne $m2ts_file.Length ){
                    $copy_flag = $true
                }

                if($copy_flag)
                {
                    [timespan]$time_diff = [DateTime]::Now - $m2ts_file.LastWriteTime
                    if( $time_diff.TotalSeconds -lt $recording_detection_duration ){
                        $copy_flag = $false
                    } elseif ( (${backup_m2ts_within_day} -gt 0 ) -and ( $time_diff.TotalDays -ge $backup_m2ts_within_day ) ){
                        $copy_flag = $false
                    }
                    else
                    {
                        $find_flag = $false
                        foreach( $destination_file in $destination_files )
                        {
                            if( ( Split-Path -Leaf $m2ts_file ).Equals( ( Split-Path -Leaf $destination_file ) ) )
                            {
                                $find_flag = $true
                                break
                            }
                        }

                        if( !$find_flag )
                        {
                            $copy_files += $m2ts_file.FullName
                            $destination_files += $m2ts_file.FullName.Replace($tentative_parent,$destination)
                        }
                    }
                } 
            }
        }
    }

    if( $copy_files.Count -gt 0 )
    {
        [string]$console_output = "以下のファイルをバックアップします。"
        foreach( $copy_file in $copy_files ){
            $console_output += ( "`n  - ${copy_file}" )
        }
        OutputLog -type Info -function "BackupM2ts" -message $console_output

        if( !$script:quiet_flag )
        {
            $response = ( Read-Host "よろしいですか?([Y]es/[n]o/[q]uiet)" )
            switch($response)
            {
                "" {}
                "y" {}
                "q" { 
                    $script:quiet_flag = $true
                }
                default { return; }
            }
        }
    }

    for( [int]$i=0; $i -lt $copy_files.Count; $i++ )
    {
        BackupM2ts_OneFile -source $copy_files[$i] -destination $destination_files[$i]
    }
    
    OutputLog -type Info -function "BackupM2ts" -message "Finish backup"
}

function BackupM2ts_OneFile {
    Param(
        [string]$source,
        [string]$destination
    )

    [string]$source_file_name = ( Split-Path -Leaf $source ) 
    [string]$source_file_path = ConvertPath($source)

    [string]$destination_directory = Split-Path -Parent $destination
    if( -Not(Test-Path(${destination_directory}))){
        New-Item -Type Directory ($destination_directory) > $null
    }

    if( Test-Path(ConvertPath(${destination})) ){
        Remove-Item -Path (ConvertPath(${destination}))
    }

    [UInt64]$source_size = (Get-Item $source_file_path).Length
    OutputLog -type Info -function "BackupM2ts_OneFile" -message ( "Copy `""+$source_file_name+"`" ("+($source_size/1000/1000/1000).ToString("0.00")+"GB) ..." )

    [DateTime]$start_time = [DateTime]::Now
    if( $debug ){ OutputLog -type Debug -function "BackupM2ts_OneFile" -message "Copy-Item -Path $source_file_path -Destination $destination_directory -Force" }
    [string]$temp_destination_path = ("$destination_directory"+"\TEMP_"+$source_file_name)
    if( Test-Path(ConvertPath(${temp_destination_path})) ){
        Remove-Item -Path (${temp_destination_path})
    }
    Copy-Item -Path "$source_file_path" -Destination ${temp_destination_path} -Force
    if( Test-Path(ConvertPath(${destination})) ){
        Remove-Item -Path (ConvertPath(${destination}))
    }
    Move-Item -Path  (ConvertPath(${temp_destination_path})) -Destination ${destination} -Force
    [TimeSpan]$duration=[DateTime]::Now-$start_time
    OutputLog -type Info -function "BackupM2ts_OneFile" -message ( "Copy duration : " + (GetDurationString($duration)) )

    [string]$destination_file_path = ConvertPath($destination)
    if(-Not(Test-Path($destination_file_path))){
        OutputLog -type Error -function "BackupM2ts_OneFile" -message ( "Fail to backup : " + $source_file_name  )
    }
}

function ConvertToMp4 {
    Param(
        [string]$source,
        [string]$destination
    )

    OutputLog -type Info -function "ConvertToMp4" -message "Start converting"

    $m2ts_files = Get-ChildItem "$source" -Recurse -Filter *.m2ts

    $convert_files = @()
    foreach( $m2ts_file in $m2ts_files ){

        [bool]$convert_flag = $false

        if( !$m2ts_file.Name.StartsWith("_") -and !$m2ts_file.Name.StartsWith("TEMP_") )
        {
            [string]$destination_file_path = ConvertPath($m2ts_file.FullName.Replace($source,$destination)).Replace(".m2ts",".mp4")

            if( -Not(Test-Path $destination_file_path) ){
                $convert_flag = $true
            } elseif( (Get-Item $destination_file_path).LastWriteTime -lt $m2ts_file.CreationTime ){
                $convert_flag = $true
            } elseif( Test-Path( ConvertPath((Split-Path -Parent ${destination_file_path}) + "\TEMP_" +(Split-Path -Leaf ${destination_file_path}) ) ) ){
                $convert_flag = $true
            }
        }

        if($convert_flag){
            [string]$temp_file_path = (Split-Path -Parent ${destination_file_path}) + "\TEMP_" +(Split-Path -Leaf ${destination_file_path})
            if( Test-Path( $temp_file_path) ){
                Remove-Item -Path ${temp_file_path}
            }

            $convert_files += $m2ts_file
        }
    }

    if( $convert_files.Count -gt 0 )
    {
        [string]$console_output = "以下のファイルをMP4へ変換します。"
        foreach( $convert_file in $convert_files ){
            $console_output += ( "`n  - " + (Split-Path -Leaf $convert_file) )
        }
        OutputLog -type Info -function "ConvertToMp4" -message $console_output

        if( !$script:quiet_flag )
        {
            $response = ( Read-Host "よろしいですか?([Y]es/[n]o/[q]uiet)" )
            switch($response)
            {
                "" {}
                "y" {}
                "q" { 
                    $script:quiet_flag = $true
                }
                default { return; }
            }
        }
    }

    foreach( $convert_file in $convert_files )
    {
        ConvertToMp4_OneFile -source $convert_file.FullName -destination $convert_file.FullName.Replace($source,$destination).Replace(".m2ts",".mp4")
    }

    OutputLog -type Info -function "ConvertToMp4" -message "Finish converting"

    Remove-Item "${script:work_directory}/*" -Exclude .gitkeep -Recurse -Force > $null
}

function ConvertToMp4_OneFile {
    Param(
        [string]$source,
        [string]$destination
    )

    [string]$source_file_name = ( Split-Path -Leaf $source ) 

    [string]$destination_directory = (Split-Path -Parent $destination)
    if( -Not(Test-Path(${destination_directory}))){
        New-Item -Type Directory ${destination_directory} > $null
    }

    OutputLog -type Info -function "ConvertToMp4_OneFile" -message "Convert `"$source_file_name`" ..."

    [DateTime]$start_time_convert = [DateTime]::Now

    if( Test-Path(ConvertPath(${destination})) ){
        Remove-Item -Path (ConvertPath(${destination}))
    }
    if( Test-Path(ConvertPath(${destination}).Replace(".mp4",".ass")) ){
        Remove-Item -Path (ConvertPath(${destination}).Replace(".mp4",".ass"))
    }
    [string]$temp_file = (Split-Path -Parent ${destination}) + "\TEMP_" +(Split-Path -Leaf ${destination})

    [string]$argument_list = $script:convert_to_mp4_commandline
    $argument_list += " --input `"${source}`" "
    $argument_list += " --output `"${temp_file}`" "
    OutputLog -type Info -function "ConvertToMp4_OneFile" -message "Start-Process $script:amatsukaze_cli_exe -ArgumentList `"$argument_list`" -Wait"
    Start-Process $script:amatsukaze_cli_exe -ArgumentList "$argument_list" -Wait

    Move-Item -Path (ConvertPath(${temp_file})) -Destination (ConvertPath(${destination}))
    if( Test-Path(ConvertPath(${temp_file}).Replace(".mp4",".ass")) ){
        Move-Item -Path (ConvertPath(${temp_file}).Replace(".mp4",".ass")) -Destination (ConvertPath(${destination}).Replace(".mp4",".ass"))
    }
        
    [TimeSpan]$duration_convert=[DateTime]::Now-$start_time_convert
    OutputLog -type Info -function "ConvertToMp4_OneFile" -message ( "Convert duration : " + (GetDurationString($duration_convert) ) )
    
    [string]$destination_file_path = ConvertPath($destination)
    if(-Not(Test-Path($destination_file_path))){
        OutputLog -type Error -function "ConvertToMp4_OneFile" -message ( "Fail to convert : " + $source_file_name )
    }
}

function CopyToNas {
    Param(
        [string]$source,
        [string]$destination,
        [int]$minimum_copy_size
    )

    OutputLog -type Info -function "CopyToNas" -message "Start copying to NAS"

    $mp4_files = Get-ChildItem "$source" -Recurse -Filter *.mp4

    $copy_files = @()
    foreach( $mp4_file in $mp4_files ){

        [bool]$copy_flag = $false

        if( !$mp4_file.Name.StartsWith("_")-and !$mp4_file.Name.StartsWith("TEMP_") )
        {
            [string]$destination_file_path = ConvertPath($mp4_file.FullName.Replace($source,$destination))

            if(-Not(Test-Path $destination_file_path) ){
                $copy_flag = $true
            } elseif( (Get-Item $destination_file_path).Length -ne $mp4_file.Length ){
                $copy_flag = $true
            }

            if($copy_flag)
            {
                if( $mp4_file.Length -gt $minimum_copy_size )
                {
                    $copy_files += $mp4_file
                }
            }
        }
    }

    if( $copy_files.Count -gt 0 )
    {
        [string]$console_output = "以下のファイルをNASへコピーします。"
        foreach( $copy_file in $copy_files ){
            $console_output += ( "`n  - ${copy_file}" )
        }
        OutputLog -type Info -function "CopyToNas" -message $console_output

        if( !$script:quiet_flag )
        {
            $response = ( Read-Host "よろしいですか?([Y]es/[n]o/[q]uiet)" )

            $response = ( Read-Host $console_output ).Trim()
            switch($response)
            {
                "" {}
                "y" {}
                "q" { 
                    $script:quiet_flag = $true
                }
                default { return; }
            }
        }
    }

    foreach( $copy_file in $copy_files )
    {
        CopyToNas_OneFile -source $copy_file.FullName -destination $copy_file.FullName.Replace($source,$destination)
    }

    OutputLog -type Info -function "CopyToNas" -message "Finish copying to NAS"
}

function CopyToNas_OneFile {
    Param(
        [string]$source,
        [string]$destination
    )

    [string]$source_file_path = ConvertPath($source)
    [string]$source_file_name = ( Split-Path -Leaf $source ) 

    [string]$destination_directory = Split-Path -Parent $destination
    if( -Not(Test-Path(${destination_directory}))){
        New-Item -Type Directory ($destination_directory) > $null
    }

    [UInt64]$source_size = (Get-Item $source_file_path).Length
    OutputLog -type Info -function "CopyToNas_OneFile" -message ( "Copy `""+$source_file_name+"`" ("+($source_size/1000/1000/1000).ToString("0.00")+"GB) ..." )

    [DateTime]$start_time = [DateTime]::Now
    if( $debug ){ OutputLog -type Debug -function "CopyToNas_OneFile" -message "Copy-Item -Path $source_file_path -Destination $destination_directory -Force" }

    [string]$temp_destination_path = ("$destination_directory"+"\TEMP_"+$source_file_name)
    if( Test-Path(ConvertPath(${temp_destination_path})) ){
        Remove-Item -Path (${temp_destination_path})
    }
    Copy-Item -Path "$source_file_path" -Destination ${temp_destination_path} -Force
    if( Test-Path(ConvertPath(${destination})) ){
        Remove-Item -Path (ConvertPath(${destination}))
    }
    Move-Item -Path  (ConvertPath(${temp_destination_path})) -Destination ${destination} -Force

    if( Test-Path($source_file_path.Replace(".mp4",".ass")) ){
        Copy-Item -Path $source_file_path.Replace(".mp4",".ass") -Destination $destination_directory -Force
    }
    [TimeSpan]$duration=[DateTime]::Now-$start_time
    OutputLog -type Info -function "CopyToNas_OneFile" -message ( "Copy duration : " + (GetDurationString($duration))  )

    [string]$destination_file_path = ConvertPath($destination)
    if(-Not(Test-Path($destination_file_path))){
        OutputLog -type Error -function "CopyToNas_OneFile" -message ( "Fail to convert : " + $source_file_name  )
    }
}

function GenerateRemotePlay_ConvertFileName {
    Param(
        [string]$filepath
    )

    if(!$remoteplay_rename)
    {
        return $filepath
    }

    [string]$directory = ( Split-Path -Parent $filepath ) 
    [string]$filename = ( Split-Path -Leaf $filepath ) 
    [string]$filename_body = [System.IO.Path]::GetFileNameWithoutExtension($filename) 
    [string]$filename_extention = [System.IO.Path]::GetExtension($filename) 

    [string]$date = $filename_body.Substring(0,8)

    [string]$modified_filename_body = ( $filename_body.Substring(9) -Replace "\[.\]", "" )+ "(" + $date + ")"

    return $directory+"\" + $modified_filename_body + $filename_extention
}

function GenerateRemotePlay {
    Param(
        [string]$source,
        [string]$destination
    )

    OutputLog -type Info -function "GenerateRemotePlay" -message "Start generating remote play file"

    $mp4_files = Get-ChildItem "$source" -Recurse -Filter *.mp4

    [string[]]$convert_files = @()
    [string[]]$destination_files = @()
    foreach( $mp4_file in $mp4_files )
    {
        [bool]$convert_flag = $false
        
        if( !$mp4_file.Name.StartsWith("_") -and !$mp4_file.Name.StartsWith("TEMP_") ){

            [string]$destination_file_path = GenerateRemotePlay_ConvertFileName($mp4_file.FullName.Replace($source,$destination))

            if( -Not(Test-Path (ConvertPath($destination_file_path)) )){
                $convert_flag = $true
            } elseif( (Get-Item (ConvertPath($destination_file_path)) ).LastWriteTime -lt $mp4_file.LastWriteTime ){
                $convert_flag = $true
            } elseif( Test-Path( ConvertPath((Split-Path -Parent (ConvertPath(${destination_file_path}))) + "\TEMP_" +(Split-Path -Leaf ${destination_file_path}) ) ) ){
                $convert_flag = $true
            }
        
            if($convert_flag)
            {
                if( $mp4_file.Length -gt $copy_allow_size )
                {
                    [string]$temp_file_path = (Split-Path -Parent ${destination_file_path}) + "\TEMP_" +(Split-Path -Leaf (GenerateRemotePlay_ConvertFileName($mp4_file.FullName)))
                    if( Test-Path( ConvertPath($temp_file_path)) ){
                        Remove-Item -Path (ConvertPath(${temp_file_path}))
                    }
        
                    $convert_files += $mp4_file.FullName
                    $destination_files += $mp4_file.FullName.Replace($source,$destination)
                }
            }
        }
    }

    if( $convert_files.Count -gt 0 )
    {
        [string]$console_output = "以下のファイルをリモート視聴用ファイルに変換します。"
        foreach( $convert_file in $convert_files )
        {
            $console_output += ( "`n  - " + (Split-Path -Leaf $convert_file) + "`n      -> " + (Split-Path -Leaf (GenerateRemotePlay_ConvertFileName($convert_file))) )
        }
        OutputLog -type Info -function "GenerateRemotePlay" -message $console_output

        if( !$script:quiet_flag )
        {
            $response = ( Read-Host "よろしいですか?([Y]es/[n]o/[q]uiet)" )
            switch($response)
            {
                "" {}
                "y" {}
                "q" { 
                    $script:quiet_flag = $true
                }
                default { return; }
            }
        }
    }

    for( [int]$i=0; $i -lt $convert_files.Count; $i++ )
    {
        GenerateRemotePlay_OneFile -source $convert_files[$i] -destination $destination_files[$i]
    }
    
    Remove-Item "${script:work_directory}/*" -Exclude .gitkeep -Recurse -Force > $null

    OutputLog -type Info -function "GenerateRemotePlay" -message "Finish generating remote play file"
}

function GenerateRemotePlay_OneFile {
    Param(
        [string]$source,
        [string]$destination
    )

    [string]$source_file_name = ( Split-Path -Leaf $source ) 
    [string]$source_file_path = ConvertPath($source)

    [string]$destination_directory = (Split-Path -Parent $destination)
    if( -Not(Test-Path(${destination_directory}))){
        New-Item -Type Directory ${destination_directory} > $null
    }

    OutputLog -type Info -function "GenerateRemotePlay_OneFile" -message "Convert `"$source_file_name`" ..."
    [DateTime]$start_time_convert = [DateTime]::Now

    if( Test-Path(ConvertPath(${destination})) ){
        Remove-Item -Path (ConvertPath(${destination}))
    }

    if( Test-Path(ConvertPath(${destination}).Replace(".mp4",".ass")) ){
        Remove-Item -Path (ConvertPath(${destination}).Replace(".mp4",".ass"))
    }

    [string]$temp_file = (Split-Path -Parent ${destination}) + "\TEMP_" +(Split-Path -Leaf ${destination})

    [string]$argument_list = $script:remoteplay_commandline_nvenc
    $argument_list += " --input `"${source}`" "
    $argument_list += " --output `"${temp_file}`" "
    OutputLog -type Info -function "GenerateRemotePlay_OneFile" -message "Start-Process $script:nvencc64_exe -ArgumentList `"$argument_list`" -Wait"
    Start-Process $script:nvencc64_exe -ArgumentList "$argument_list" -Wait

    Move-Item -Path (ConvertPath(${temp_file})) -Destination (ConvertPath(${destination}))
    if( Test-Path($source_file_path.Replace(".mp4",".ass")) ){
        Copy-Item -Path $source_file_path.Replace(".mp4",".ass") -Destination $destination.Replace(".mp4",".ass") -Force
    }

    [TimeSpan]$duration_convert=[DateTime]::Now-$start_time_convert
    OutputLog -type Info -function "GenerateRemotePlay_OneFile" -message ( "Convert duration : " + (GetDurationString($duration_convert) ) )
    
    [string]$destination_file_path = ConvertPath($destination)
    if(-Not(Test-Path($destination_file_path))){
        OutputLog -type Error -function "GenerateRemotePlay_OneFile" -message ( "Fail to convert : " + $source_file_name )
    }
}

function DeleteFiles {
    Param(
        [string]$source,
        [string]$destination
    )

    OutputLog -type Info -function "DeleteFiles" -message "Start deleting files from share folder"

    $mp4_files = Get-ChildItem "$destination" -Recurse -Filter *.mp4

    [string[]]$delete_files = @()
    foreach( $mp4_file in $mp4_files ){

        if( !$mp4_file.Name.StartsWith("_") ){

            $source_file_path = ConvertPath($mp4_file.FullName.Replace($destination,$source))
    
            if(-Not(Test-Path $source_file_path) ) {
                $delete_files += $mp4_file.FullName
            }
        }
    }

    if( $delete_files.Count -gt 0 )
    {
        [string]$console_output = "以下のファイルを共有フォルダから削除します。`n"
        foreach( $delete_file in $delete_files )
        {
            $console_output += ( "  - " + (Split-Path -Leaf $delete_file) + "`n" )
        }
        $console_output += "よろしいですか?([y]es/[N]o)"

        $response = ( Read-Host $console_output ).Trim()
        switch($response)
        {
            "y" {}
            default { return; }
        }
    }

    foreach( $delete_file in $delete_files )
    {
        Remove-Item -Path (ConvertPath($delete_file))
        if( Test-Path(ConvertPath($delete_file).Replace(".mp4",".ass")) ){
            Remove-Item -Path (ConvertPath($delete_file).Replace(".mp4",".ass"))
        }

        [string]$parent_folder_path = (Split-Path -Parent $delete_file)
        [int]$number_of_mp4_file = (Get-ChildItem $parent_folder_path -Recurse -Filter *.mp4 -ErrorAction Stop | Measure-Object).Count
        if ( $number_of_mp4_file -eq 0){  
            Remove-Item -Path (ConvertPath($parent_folder_path)) -Force -Recurse
        } 
    }
}

#########################
# Parameter check
#########################
if( -Not(Test-Path "${script:amatsukaze_directory}") ){
    OutputLog -type Error -function "ParameterCheck" -message ( "No Amatsukaze folder '"+${script:amatsukaze_director}+"'")
    exit
}

if( $backup_m2ts )
{
    foreach( $source_directory in ${script:source_directory_root} )
    {
        if( -Not(Test-Path "${source_directory}") ){
            OutputLog -type Error -function "ParameterCheck" -message ( "No m2ts folder '"+${source_directory}+"'")
            exit
        }
    }

    foreach( $tentative_directory in ${script:tentative_directory_root} )
    {
        if( -Not(Test-Path "${tentative_directory}") ){
            OutputLog -type Error -function "ParameterCheck" -message ( "No m2ts folder '"+${tentative_directory_root}+"'")
            exit
        }
    }
}

if( $copy_to_nas -and -Not(Test-Path "${script:nas_directory_root}") ){
    OutputLog -type Error -function "ParameterCheck" -message ( "No NAS folder '"+${script:nas_directory_root}+"'")
    exit
}

if( $generate_remoteplay -and -Not(Test-Path "${script:remote_play_directory_nas_root}") ){
    OutputLog -type Error -function "ParameterCheck" -message ( "No NAS folder '"+${script:remote_play_directory_nas_root}+"'")
    exit
}

if( -Not(Test-Path "${script:work_directory}") ){
    New-Item -Type Directory ${script:work_directory} > $null
    if( -Not(Test-Path "${script:work_directory}") ){
        OutputLog -type Error -function "ParameterCheck" -message ( "Cannot create work directory '"+${script:work_directory}+"'")
        exit
    }
}

$log_file_dir = ( Split-Path -Parent ${script:log_file_path} )
if( -Not(Test-Path (${log_file_dir}) ) ){
    New-Item -Type Directory ${log_file_dir} > $null
}

#########################
# Main
#########################
OutputLog -type Info -message "Start processing"

Remove-Item "${script:work_directory}/*" -Exclude .gitkeep -Recurse -Force > $null

# Backup m2ts
if( $backup_m2ts ){
    if( -Not(Test-Path "${script:backup_directory_root}") ){
        New-Item -Type Directory ${script:backup_directory_root} > $null
    }

    BackupM2ts -sources ${script:source_directory_root} -tentatives ${script:tentative_directory_root} -destination ${script:backup_directory_root}
}

# convert
if( $convert_to_mp4 ){
    if( -Not(Test-Path "${script:convert_directory_root}") ){
        New-Item -Type Directory ${script:convert_directory_root} > $null
    }
    ConvertToMp4 -source "$script:backup_directory_root" -destination "$script:convert_directory_root"
}

# Copy to NAS
if( $copy_to_nas ){
    CopyToNas -source "$script:convert_directory_root" -destination "$script:nas_directory_root" -minimum_copy_size $copy_allow_size
}

# convert
if( $generate_remoteplay ){
    if( -Not(Test-Path "${script:remote_play_directory_root}") ){
        New-Item -Type Directory ${script:remote_play_directory_root} > $null
    }
    GenerateRemotePlay -source "$script:convert_directory_root" -destination "$script:remote_play_directory_root"
    CopyToNas -source "$script:remote_play_directory_root" -destination "$script:remote_play_directory_nas_root" -minimum_copy_size $copy_allow_size_remote_play
}

# delete
if( $delete ){
    DeleteFiles -source "$script:convert_directory_root" -destination "$script:nas_directory_root"
    DeleteFiles -source "$script:remote_play_directory_root" -destination "$script:remote_play_directory_nas_root"
}

#shutdown
if( $shutdown ){
    for( [int]$i=$delay_shutdown;$i -gt 0;$i-=10){
        Write-Output ([string]::Format("[$script:script_name] {0} : Info : Shutdown in ${i} sec.", ((Get-Date).ToString("yyyy/MM/dd HH:mm:ss")) ) ) | Tee-Object -Append -FilePath $script:log_file_path
        Start-Sleep -s 10
    }
    Write-Output ([string]::Format("[$script:script_name] {0} : Info : Shutdown", ((Get-Date).ToString("yyyy/MM/dd HH:mm:ss")) ) ) | Tee-Object -Append -FilePath $script:log_file_path
    shutdown.exe /s -t 5
}

OutputLog -type Info -message "Finish processing"