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

※2024/09/29 スクリプトのリファクタリングを行いました

TS抜きしたM2TSファイルは1時間番組でもファイルサイズが7GBを超えるため、録画後にバックアップしたりコピーしたりする際に時間がかかる上にストレージも圧迫します。さらに再生用ソフトも限定されるため、視聴にも制限がかかります。この問題はMP4へ変換してファイルサイズを落とすことで解決できます。

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

また、変換の際にCMを抜くとさらにファイルサイズを落とせます。通常2割弱程度がCMですので、圧縮後のファイルからさらに2割サイズを落とすことができます。

しかし、録画した番組を保存する際に必要とされる機能はファイルの変換だけでなく、変換したファイルをNASへコピーしたり、古いファイルを削除してHDDの容量を確保したりするなど、自動化に際して様々な要求が出てきます。

そこで、これらの作業の自動化には、CMカットとM2TS→MP4変換にWindows上で動作するAmatsukazeを利用し、Powershellでファイルのコピー・削除処理を行う一連の処理を自動化します。

PowershellはWindowsのデフォルトでインストールされているシェルなので、Pythonのように別途ソフトをインストールす必要がなく、どのPCでも簡単に実行環境をセットアップできる利点があります。

スクリプトはコピー・変換・削除の機能をそれぞれスクリプトファイルを分けて実装し、バッチファイル側で機能を選択する形で様々なパターンに対応できるようにしています。これらのスクリプトは以下の処理を自動化します。

  1. M2TSファイルのコピーをとる
  2. Amatsukazeを使ってCM抜き処理とmp4変換を実行する
  3. 最終書込日時が古いファイルを削除する

このスクリプト群は下記の機能を実装しています。

  • すでに変換・コピー済みのm2tsファイルの処理はスキップする
  • コピー・変換中にスケジューラーなどで変換・コピー処理を再実行した場合は処理をスキップする
  • 録画中のM2TSはバックアップしない
  • ファイルコピー途中に強制終了した場合には次回実行時に再度コピーを行う
  • 変換途中に強制終了させた場合は次回実行時に再度変換処理を行う
  • ファイル名の先頭に’_’をつけると、変換・コピーの対象外になる
  • ファイル削除後にフォルダが空になった場合はフォルダも削除する
  • 指定したフォルダサイズが指定容量を超えた場合は最終書き込み日時の古いファイルから順番に削除して、自動的に指定したフォルダサイズをキープする

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

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

AmatsukazeCli.exeの実行中のメッセージが文字化けして読めない場合は、下記のシステムロケール設定を日本語にすると正しく表示されるようになります。

参考になれば幸いです。

@echo off
chcp 65001

@REM Initialize batch
SET CURRENT_DIR_CONVERT_BAT=%~dp0
cd /d %CURRENT_DIR_CONVERT_BAT%
SET BATCH_NAME_CONVERT_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_DIR=%CURRENT_DIR_CONVERT_BAT%
SET LOG_FILE_PATH=S:\log\%CUR_DATE%_%CUR_TIME%_%~n0.log
SET POWERSHELL_SCRIPT_BACKUP=%POWERSHELL_SCRIPT_DIR%Backup.ps1
SET POWERSHELL_SCRIPT_DELETE_DOT_UNDERSCORE_FILES=%POWERSHELL_SCRIPT_DIR%DeleteDotUnderScoreFiles.ps1
SET POWERSHELL_SCRIPT_COPY_M2TS=%POWERSHELL_SCRIPT_DIR%CopyM2ts.ps1
SET POWERSHELL_SCRIPT_COPY_CONVERT=%POWERSHELL_SCRIPT_DIR%Convert.ps1
SET POWERSHELL_SCRIPT_COPY_MP4=%POWERSHELL_SCRIPT_DIR%CopyMp4.ps1
SET POWERSHELL_SCRIPT_DELETE_FILES=%POWERSHELL_SCRIPT_DIR%DeleteFile.ps1

@REM Copy Record files to NAS
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_COPY_M2TS%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\192.168.1.6\tv-recorder\recorded_files\delete"
SET ARGUMENT= %ARGUMENT% -destination "\\bluesky-nas\tv_program_backup\m2ts\delete"
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -delete
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Copy Record files to NAS
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_COPY_M2TS%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\192.168.1.6\tv-recorder\recorded_files\keep"
SET ARGUMENT= %ARGUMENT% -destination "\\bluesky-nas\tv_program_backup\m2ts\keep"
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -delete
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Convert files
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_COPY_CONVERT%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\bluesky-nas\tv_program_backup\m2ts"
SET ARGUMENT= %ARGUMENT% -destination "\\bluesky-nas\tv_program\converted_files"
@REM SET ARGUMENT= %ARGUMENT% -nvenc
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Delete old converted files from NAS
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_DELETE_FILES%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\bluesky-nas\tv_program\converted_files\delete"
SET ARGUMENT= %ARGUMENT% -day 365
SET ARGUMENT= %ARGUMENT% -totalsize 2000
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Delete old recorded files from NAS
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_DELETE_FILES%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\bluesky-nas\tv_program_backup\m2ts\delete"
SET ARGUMENT= %ARGUMENT% -day 365
SET ARGUMENT= %ARGUMENT% -totalsize 3000
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Delete unnecessary files/folders(tv_program_backup)
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_DELETE_FILES%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\bluesky-nas\tv_program_backup\m2ts"
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Delete unnecessary files/folders(tv_program_backup)
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_DELETE_FILES%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\bluesky-nas\tv_program\converted_files"
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Delete old recorded files from Media-player
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_DELETE_FILES%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "S:\recorded_files\delete"
SET ARGUMENT= %ARGUMENT% -day 180
SET ARGUMENT= %ARGUMENT% -totalsize 600
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Backup to NAS(TV recorder)
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_BACKUP%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "\\192.168.1.6\tv-recorder\backup"
SET ARGUMENT= %ARGUMENT% -destination "\\bluesky-nas\pc_backup\tv-recorder1"
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM Backup to NAS(Media-player)
SET POWERSHELL_SCRIPT=%POWERSHELL_SCRIPT_BACKUP%
SET ARGUMENT=
SET ARGUMENT= %ARGUMENT% -log_file "%LOG_FILE_PATH%"
SET ARGUMENT= %ARGUMENT% -source "D:\\"
SET ARGUMENT= %ARGUMENT% -destination "\\bluesky-nas\pc_backup\media-player\D_Drive"
SET ARGUMENT= %ARGUMENT% -quiet
@REM SET ARGUMENT= %ARGUMENT% -shutdown
@echo [%BATCH_NAME_CONVERT_BAT%] powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%
call powershell -NoProfile -ExecutionPolicy Bypass -file %POWERSHELL_SCRIPT% -ArgumentList %ARGUMENT%

@REM pause
###################
# Enumuration
###################
enum OutputLogType {
    Info
    Warning
    Error
    Debug
    NoType
}

###################
# Class
###################
class Entry{

    [string]$value
    [string]$comment

    Entry( [string]$value ){
        $this.value = $value
        $this.comment = ""
    }

    Entry( [string]$value, [string]$comment ){
        $this.value = $value
        $this.comment = $comment
    }

    UpdateValue( [string]$value ){
        $this.value = $value
    }

    UpdateComment( [string]$comment ){
        $this.comment = $comment
    }
}

class IniFileManager {

    [System.Collections.Specialized.OrderedDictionary]$ini_contents = [System.Collections.Specialized.OrderedDictionary]@{}
    [string]$ini_file_path = ""

    IniFileManager($ini_file_path) {

        $this.ini_file_path = $ini_file_path

        [IniFileManager]::ReadFromIniFile($this.ini_file_path,$this.ini_contents)
    }

    static ReadFromIniFile( [string]$ini_file_path, [System.Collections.Specialized.OrderedDictionary]$ini_contents ){
        [int]$comment_count = 0
        [string]$section=""

        if( Test-Path "${ini_file_path}" ){
            switch -regex -file $ini_file_path {
                "^(\s*;.*)$" {
                    $value = $matches[1]
                    $comment_count = $comment_count + 1
                    $entry = "=Comment" + $comment_count + "="
                    if( [string]::IsNullOrEmpty($section) ) {
                        $section = "NoSection"
                        $ini_contents[$section] = [System.Collections.Specialized.OrderedDictionary]@{}
                    }
                    $ini_contents[$section][$entry] = [Entry]::new($value)
                    continue
                }
                "^\[(.+)\]" {
                    $section = $matches[1]
                    $ini_contents[$section] = [System.Collections.Specialized.OrderedDictionary]@{}
                    $comment_count = 0
                    continue
                }
                "(.+?)\s*=(.*)" {
                    $entry = $matches[1].Trim()
                    $data = $matches[2]
                    $value = ""
                    $comment = ""
                    $data_without_string_in_double_quatation = ( $data.Replace("\","\\") -replace "`"((?:[^\\`"]+|\\.)*)`"","")
                    if( $data_without_string_in_double_quatation.IndexOf(";") -eq -1 ){
                        # No comment in entry
                        $value = $data.Trim()
                    } else {
                        $comment = ($data_without_string_in_double_quatation -replace "^.*?(?=;)","" )
                        $value = $data.Replace( $comment,"" ).Trim()
                    }
                    if( [string]::IsNullOrEmpty($section) ) {
                        $section = "NoSection"
                        $ini_contents[$section] = [System.Collections.Specialized.OrderedDictionary]@{}
                    }
                    $ini_contents[$section][$entry] = [Entry]::new($value,$comment)
                    continue
                }
            }
        }
    }

    Merge( [string]$ini_file_path ){

        [System.Collections.Specialized.OrderedDictionary]$ini_contents_merge = [System.Collections.Specialized.OrderedDictionary]@{}

        [IniFileManager]::ReadFromIniFile($ini_file_path,$ini_contents_merge)

        foreach( $section in $ini_contents_merge.Keys ){
            foreach( $entry in $ini_contents_merge[$section].Keys ){
                if( $null -eq $this.ini_contents[$section] ){
                    $this.ini_contents[$section] += [System.Collections.Specialized.OrderedDictionary]@{}
                }
        
                $data=$ini_contents_merge[$section][$entry]

                if( $null -eq $this.ini_contents[$section][$entry] ){
                    $this.ini_contents[$section].Add($entry,$data)
                } else {
                    $this.ini_contents[$section][$entry].UpdateValue($data.value)
                }
            }
        }
    }

    [bool] IsValidSection( [string]$section ) {
        return ( $null -ne $this.ini_contents[$section] )
    }

    [bool] IsValidEntry( [string]$section, [string]$entry ) {
        return $this.IsValidEntry($section, $entry, [ValueType]::String )
    }

    [bool] IsValidEntry( [string]$section, [string]$entry, [ValueType]$value_type ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                switch($value_type){
                    [ValueType]::Long {
                        return [long]::TryParse($this.ini_contents[$section][$entry].value,[ref]$null) 
                    }
                    [ValueType]::Double {
                        return [Double]::TryParse($this.ini_contents[$section][$entry].value,[ref]$null) 
                    }
                    [ValueType]::Bool {
                        return ( $this.ini_contents[$section][$entry].value.Equals("false") -or $this.ini_contents[$section][$entry].value.Equals("true") )
                    }
                    default {#[ValueType]::string
                        return $true 
                    }
                }
            }
        }
        return $false
    }

    [bool] GetValueBool( [string]$section, [string]$entry ) {
        return $this.GetValueBool($section, $entry, $false)
    }

    [bool] GetValueBool( [string]$section, [string]$entry, [bool]$default ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                return $this.ini_contents[$section][$entry].value.Equals("true")
            }
        }

        Write-Host "[IniFileManager::GetValueBool] Forbidden route : Undefined section/entry (Section:'${section}'/Entry:'${entry}')"

        return $default
    }

    [string] GetValueString( [string]$section, [string]$entry) {
        return $this.GetValueString($section, $entry, "")
    }

    [string] GetValueString( [string]$section, [string]$entry, [string]$default ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                return $this.ini_contents[$section][$entry].value
            }
        }

        Write-Host "[IniFileManager::GetValueString] Forbidden route : Undefined section/entry (Section:'${section}'/Entry:'${entry}')"

        return $default
    }

    [long] GetValueLong( [string]$section, [string]$entry) {
        return $this.GetValueLong($section,$entry,0)
    }

    [long] GetValueLong( [string]$section, [string]$entry, [long]$default ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                [long] $ret=0
                [string] $value = $this.ini_contents[$section][$entry].value
                if( ![long]::TryParse($value,[ref]$ret) ){
                    Write-Host "[IniFileManager::GetValueLong] Forbidden route : Cannot convert '[${section}] ${entry}=${value}'"
                    $ret = 0
                }
                return $ret
            }
        }
        return $default
    }

    [double] GetValueDouble( [string]$section, [string]$entry ) {
        return $this.GetValueDouble( $section, $entry, 0.0 )
    }

    [double] GetValueDouble([string]$section, [string]$entry, [double]$default ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                [double] $ret=0
                [string] $value = $this.ini_contents[$section][$entry].value
                if( ![double]::TryParse($value,[ref]$ret) ){
                    Write-Host "[IniFileManager::GetValueDouble] Forbidden route : Cannot convert '[${section}] ${entry}=${value}'"
                    $ret = 0
                }
                return $ret
            }
        }

        Write-Host "[IniFileManager::GetValueDouble] Forbidden route : Undefined section/entry (Section:'${section}'/Entry:'${entry}')"

        return $default
    }

    [string[]] GetValueStringArray([string]$section, [string]$entry ) {
        return $this.GetValueStringArray( $section, $entry, ",", @() )
    }

    [string[]] GetValueStringArray([string]$section, [string]$entry, [string]$delimiter ) {
        return $this.GetValueStringArray( $section, $entry, $delimiter, @() )
    }

    [string[]] GetValueStringArray([string]$section, [string]$entry, [string]$delimiter, [string[]]$default ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                if( [string]::IsNullOrEmpty($this.ini_contents[$section][$entry].value) ){
                    return [string[]]@()
                } else {
                    return $this.ini_contents[$section][$entry].value.Split($delimiter)
                }
            }
        }

        Write-Host "[IniFileManager::GetValueStringArray] Forbidden route : Undefined section/entry (Section:'${section}'/Entry:'${entry}')"

        return $default
    }

    [System.Collections.Specialized.OrderedDictionary] GetEntryItems( [string]$section ) {

        [System.Collections.Specialized.OrderedDictionary]$ret=@{}

        if( $null -ne $this.ini_contents[$section] ){
            foreach( $entry in $this.ini_contents[$section].Keys ){
                $ret.Add($entry,$this.ini_contents[$section][$entry].value)
            }
        } else {
            Write-Host "[IniFileManager::GetEntryItems] Forbidden route : Undefined section (Section:'${section}')"
        }

        return $ret
    }

    Update([string]$section, [string]$entry, [bool]$value ) {
        if($value){
            $this.Update( $section, $entry, "true" )
        } else {
            $this.Update( $section, $entry, "false" )
        }
    }

    Update([string]$section, [string]$entry, [string]$value ) {

        if( $null -eq $this.ini_contents[$section] ){
            $this.ini_contents[$section] += [System.Collections.Specialized.OrderedDictionary]@{}
        }

        if( $null -eq $this.ini_contents[$section][$entry] ){
            # $this.ini_contents[$section].Add($entry,[Entry]::new($value))
            $this.ini_contents[$section].Insert(0,$entry,[Entry]::new($value))
        } else {
            $this.ini_contents[$section][$entry].value = $value
        }
    }

    UpdateComment([string]$section, [string]$entry, [string]$comment ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                $this.ini_contents[$section][$entry].comment = $comment
                return
            }
        }

        Write-Host "[IniFileManager::UpdateComment] Forbidden route : Undefined section/entry (Section:'${section}'/Entry:'${entry}')"
    }

    RemoveSection( [string]$section ) {
        if( $null -ne $this.ini_contents[$section] ){
            $this.ini_contents.Remove( $section )
        } else {
            Write-Host "[IniFileManager::RemoveSection] Forbidden route : Undefined section (Section:'${section}')"
        }
    }

    RemoveEntry( [string]$section, [string]$entry ) {
        if( $null -ne $this.ini_contents[$section] ){
            if( $null -ne $this.ini_contents[$section][$entry] ){
                $this.ini_contents[$section].Remove( $entry )
            }
        }

        Write-Host "[IniFileManager::RemoveEntry] Forbidden route : Undefined section/entry (Section:'${section}'/Entry:'${entry}')"
    }

    Write() {

        $temp_ini_file = $this.ini_file_path + ".tmp"

        if( Test-Path "${temp_ini_file}" ){
            Remove-Item "${temp_ini_file}"
        }

        $stream_writer = New-Object System.IO.StreamWriter($temp_ini_file,[System.Text.Encoding]::GetEncoding("utf-8"))

        [bool]$first_line=$true
        foreach( $section in $this.ini_contents.Keys ){
            if( -Not($section.Equals("NoSection") ) ){
                if($first_line){
                    $stream_writer.Write("[$section]`n")
                } else {
                    $stream_writer.Write("`n[$section]`n")
                }
            }

            $first_line=$false

            [long]$max_entry_length = 0
            foreach( $entry in $this.ini_contents[$section].Keys ){
                $line = ($entry+"="+$this.ini_contents[$section][$entry].value)
                $full_width_count = [regex]::Matches($line, "[^\x00-\xff]").Count
                $half_width_count = $line.Length - $full_width_count
                $data_length = ( $full_width_count *2 ) + $half_width_count
                if( $max_entry_length -lt $data_length ){
                    $max_entry_length = $data_length
                }
            }

            foreach( $entry in $this.ini_contents[$section].Keys ){

                $value   = $this.ini_contents[$section][$entry].value
                $comment = $this.ini_contents[$section][$entry].comment

                if( $entry -cmatch "^=.*=$" ){
                    if( $entry -cmatch "^=Comment[1-9][0-9]*=$" ){
                        $stream_writer.WriteLine("${value}")
                    }
                } else {
                    if( [string]::IsNullOrEmpty($comment) ){
                        $stream_writer.WriteLine("${entry}=${value}")
                    } else {
                        $line = ($entry+"="+$this.ini_contents[$section][$entry].value)
                        $full_width_count = [regex]::Matches($line, "[^\x00-\xff]").Count
                        $half_width_count = $line.Length - $full_width_count
                        $data_length = ( $full_width_count * 2 ) + $half_width_count
                        $stream_writer.WriteLine( $line + " "*($max_entry_length-$data_length) + " ;" + $comment )
                    }
                }
            }
        }

        $stream_writer.Close()

        $updated_ini_file_path=$this.ini_file_path
        if( Test-Path "${updated_ini_file_path}" ){
            Remove-Item "${updated_ini_file_path}"
        }
        Move-Item "${temp_ini_file}" "${updated_ini_file_path}"
    }
}

class Log {

    [string]$script_name
    [string]$log_file_path

    Log(
        [string]$script_name,
        [string]$log_file_path = $null
    ){
        $this.script_name = $script_name
        $this.log_file_path = $log_file_path

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

    Write(
        [OutputLogType]$type,
        [string]$function,
        [string]$message
    ){
        [string]$output = ""

        if( ![string]::IsNullOrEmpty($this.script_name) -and ![string]::IsNullOrEmpty($function) ){
            $output += [string]::Format("[{0}:{1}] ", $this.script_name, $function )
        } elseif( ![string]::IsNullOrEmpty($this.script_name)){
            $output += [string]::Format("[{0}] ", $this.script_name )
        } else {
            $output += [string]::Format("[{0}] ", $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 )

        Write-Host $output
        if(![string]::IsNullOrEmpty($this.log_file_path)){
            Add-Content -Path $this.log_file_path -Value $output -Encoding utf8
        }
    }
}

###################
# Function
###################
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
}

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 Shutdown([log]$log) {
    [int]$wait_time = 180

    for( [int]$i=$wait_time;$i -gt 0;$i-=10){
        $log.Write( [OutputLogType]::Info, "", [string]::Format("${i}秒後にシャットダウンします", ((Get-Date).ToString("yyyy/MM/dd HH:mm:ss")) ) )
        Start-Sleep -s 10
    }
    $log.Write( [OutputLogType]::Info, "", [string]::Format("シャットダウンします", ((Get-Date).ToString("yyyy/MM/dd HH:mm:ss")) ) )
    shutdown.exe /s -t 5
}

function CheckAlreadyRunning {
    [int]$number_of_process = (Get-Process -Name powershell | Where-Object -FilterScript {$_.Id -ne $PID}).Count

    return ($number_of_process -gt 1)
}
Using Module ".\Utils.psm1"

Param(
    [string]$log_file,
    [string]$source,
    [string]$destination,
    [switch]$shutdown,
    [switch]$help
)

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

if([string]::IsNullOrEmpty($log_file)){
    $log_file_path = $script:script_path+((Get-Date).ToString("\\yyyyMMdd_HHmmss_"))+$script_name.Replace(".ps1",".log")
    [Log]$script:log=[Log]::new($script:script_name,$log_file_path)
} else {
    [Log]$script:log=[Log]::new($script:script_name,$log_file)
}

[string]$script:source_root      = $source
[string]$script:destination_root = $destination

[bool]$debug=$false
if($debug){
    # [string]$script:source_root      = "${script:script_path}\files\m2ts"
    # [string]$script:destination_root = "${script:script_path}\files\m2ts_backup"
    [string]$script:source_root      = "D:\"
    [string]$script:destination_root = "\\bluesky-nas\pc_backup\media-player\D_Drive"

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

#########################
# Help
#########################
if( $help ) {
    Write-Host "NAME"
    Write-Host ""
    Write-Host "   $script:script_name - ファイルをバックアップする"
    Write-Host ""
    Write-Host "SYPNOSYS"
    Write-Host ""
    Write-Host "   $script:script_name -log_file <file path>"
    Write-Host "                       -source <directory path>"
    Write-Host "                       -destination <directory path>"
    Write-Host "                       [-quiet] [-help]"
    Write-Host ""
    Write-Host "DESCRIPTION"
    Write-Host ""
    Write-Host "   -source"
    Write-Host "        バックアップ元のファイルが格納されているディレクトリを指定する"
    Write-Host ""
    Write-Host "   -destination"
    Write-Host "        バックアップ先のディレクトリを指定する"
    Write-Host ""
    Write-Host "   -shutdown"
    Write-Host "        バックアップ終了後、${delay_shutdown}秒後にPCをシャットダウンする"
    Write-Host ""
    Write-Host "   -help"
    Write-Host "        ヘルプを表示する"
    Write-Host ""
    exit
}

#########################
# Parameter check
#########################
if( [string]::IsNullOrEmpty("${script:source_root}") -or -Not(Test-Path "${script:source_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "バックアップ元フォルダがありません '"+${script:source_root}+"'") )
    exit
}

if( [string]::IsNullOrEmpty("${script:destination_root}") -or -Not(Test-Path "${script:destination_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "バックアップ先フォルダがありません '"+${script:destination_root}+"'") )
    exit
}

#########################
# Main
#########################
$script:log.Write( [OutputLogType]::Info, "", "【バックアップを開始します】" )
$script:log.Write( [OutputLogType]::Info, "", "  バックアップ元フォルダ:${script:source_root}" )
$script:log.Write( [OutputLogType]::Info, "", "  バックアップ先フォルダ:${script:destination_root}" )

if( !$debug -and ( CheckAlreadyRunning ) ){
    $script:log.Write( [OutputLogType]::Info, "", "変換・コピー処理実行中のため中断します" )
    $script:log.Write( [OutputLogType]::Info, "", "【バックアップを終了しました】" )
    exit
}

[DateTime]$start_time = [DateTime]::Now
[string]$backup_log_file=$log_file.Replace(".log","_backup.log")
robocopy "$script:source_root" "$script:destination_root" /MIR /R:0 /W:0 /NP /TEE /XJD /XJF /DCOPY:DAT /NDL /XD $RECYCLE.BIN /UNILOG+:NUL /TEE >> $backup_log_file 

[TimeSpan]$duration=[DateTime]::Now-$start_time
$script:log.Write( [OutputLogType]::Info, "Main", ( "バックアップ処理時間 : " + (GetDurationString($duration)) ) )

#shutdown
if( $shutdown ){
    Shutdown($script:log)
}

$script:log.Write( [OutputLogType]::Info, "", "【バックアップを終了しました】" )
Using Module ".\Utils.psm1"

Param(
    [string]$log_file,
    [string]$source,
    [string]$destination,
    [switch]$quiet,
    [switch]$shutdown,
    [switch]$help
)

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

if([string]::IsNullOrEmpty($log_file)){
    $log_file_path = $script:script_path+((Get-Date).ToString("\\yyyyMMdd_HHmmss_"))+$script_name.Replace(".ps1",".log")
    [Log]$script:log=[Log]::new($script:script_name,$log_file_path)
} else {
    [Log]$script:log=[Log]::new($script:script_name,$log_file)
}

[string]$script:source_root      = $source
[string]$script:destination_root = $destination

[bool]$script:quiet_flag=$quiet

[string]$script:ini_file_path     = "${script:script_path}\CopyM2ts.ini"
[string]$script:section_copy_m2ts = $script:script_name

[bool]$debug=$false
if($debug){
    #[string]$script:source_root      = "${script:script_path}\files\m2ts"
    #[string]$script:destination_root = "${script:script_path}\files\m2ts_backup"
    [string]$script:source_root      = "\\192.168.1.6\tv-recorder\recorded_files\delete"
    [string]$script:destination_root = "\\bluesky-nas\tv_program_backup\m2ts\delete"
    [bool]$script:quiet_flag         = $true

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

[int]$recording_detection_duration = 1*60  #Unit:sec

#########################
# Help
#########################
if( $help ) {
    Write-Host "NAME"
    Write-Host ""
    Write-Host "   $script:script_name - M2TSファイルをコピーする"
    Write-Host ""
    Write-Host "SYPNOSYS"
    Write-Host ""
    Write-Host "   $script:script_name -log_file <file path>"
    Write-Host "                       -source <directory path>"
    Write-Host "                       -destination <directory path>"
    Write-Host "                       [-quiet] [-help]"
    Write-Host ""
    Write-Host "DESCRIPTION"
    Write-Host ""
    Write-Host "   -source"
    Write-Host "        M2TSファイルが格納されているディレクトリを指定する"
    Write-Host ""
    Write-Host "   -destination"
    Write-Host "        M2TSファイルのコピー先のディレクトリを指定する"
    Write-Host ""
    Write-Host "   -quiet"
    Write-Host "        処理開始前の確認をスキップする"
    Write-Host ""
    Write-Host "   -shutdown"
    Write-Host "        変換終了後、${delay_shutdown}秒後にPCをシャットダウンする"
    Write-Host ""
    Write-Host "   -help"
    Write-Host "        ヘルプを表示する"
    Write-Host ""
    exit
}

###################
# Function
###################
function CopyM2ts {
    Param(
        [string]$source,
        [string]$destination
    )

    [IniFileManager]$ini_file = [IniFileManager]::new($script:ini_file_path)

    $copy_files = @()

    $src_m2ts_files = Get-ChildItem  "$source" -Recurse -Filter *.m2ts -Exclude "lost+found"
    $dest_m2ts_files = Get-ChildItem "$destination" -Recurse -Filter *.m2ts

    foreach( $src_m2ts_file in $src_m2ts_files ){

        if( !$src_m2ts_file.Name.StartsWith(".") -and !$src_m2ts_file.Name.StartsWith("_") ){

            [bool]$copy_flag = $false

            [string]$destination_file_path = $src_m2ts_file.FullName.Replace($source,$destination)
            $dest_m2ts_file = CopyM2ts_GetDestFile -file_path $destination_file_path -dest_files $dest_m2ts_files
    
            if( $null -eq $dest_m2ts_file ){
                $copy_flag = $true
            } elseif( $src_m2ts_file.Length -ne $dest_m2ts_file.Length ){
                $copy_flag = $true
            } else {
                [string]$temp_file_path = ( Split-Path -Parent ${destination_file_path}) + "\TEMP_" +(Split-Path -Leaf ${destination_file_path})
                $temp_file_path = CopyM2ts_GetDestFile -file_path $temp_file_path -dest_files $dest_m2ts_files
                if( -Not([string]::IsNullOrEmpty($temp_file_path.FullName) ) ){
                    $copy_flag = $true
                }
            }
        }

        if($copy_flag){
            [timespan]$time_diff = [DateTime]::Now - $src_m2ts_file.LastWriteTime
            if( $time_diff.TotalSeconds -lt $recording_detection_duration ){
                $script:log.Write( [OutputLogType]::Info, "CopyM2ts", ( "スキップ(録画中) .................... " + $src_m2ts_file.Name ) )
                $copy_flag = $false
            } elseif( $src_m2ts_file.Length -eq $ini_file.GetValueLong( $script:section_copy_m2ts, $src_m2ts_file.Name, -1 ) ){ `
                if( $debug ){ $script:log.Write( [OutputLogType]::Info, "CopyM2ts", ( "スキップ(コピー済み) .... " + $src_m2ts_file.Name ) ) }
                $copy_flag = $false
            } else {

                [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}
                }

                $copy_files += $src_m2ts_file
            }
        }
    }

    if( $copy_files.Count -gt 0 ) {
        [string]$console_output = "以下のファイルをコピーします。"
        foreach( $copy_file in $copy_files ){
            $console_output += ( "`n  - ${copy_file}" )
        }
        $console_output += "`n  コピー先:$destination"
        $script:log.Write( [OutputLogType]::Info, "CopyM2ts", $console_output )

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

    foreach( $copy_file in $copy_files ){

        $destination_file = $copy_file.FullName.Replace($source,$destination)
        CopyM2ts_OneFile -source $copy_file.FullName -destination $destination_file

        if( Test-Path(ConvertPath($destination_file)) ){
            $m2ts_file_copy = (Get-Item -Path ( ConvertPath($copy_file.FullName) ) )
            $ini_file.Update( $script:section_copy_m2ts, $m2ts_file_copy.Name, $m2ts_file_copy.Length )
            $ini_file.Write()
        }
    }
}

function CopyM2ts_GetDestFile {
    Param(
        [string]$file_path,
        [System.Array]$dest_files
    )

    foreach( $dest_file in $dest_files ){
        if( $file_path.Equals($dest_file.FullName) ){
            return $dest_file
        }
    }

    return $null
}

function CopyM2ts_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
    $script:log.Write( [OutputLogType]::Info, "CopyM2ts_OneFile", ( "Copy `""+$source_file_name+"`" ("+($source_size/1000/1000/1000).ToString("0.00")+"GB) ..." ) )

    [DateTime]$start_time = [DateTime]::Now
    if( $debug ){ $script:log.Write( [OutputLogType]::Debug, "CopyM2ts_OneFile", "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}))
    }

    if( Test-Path(ConvertPath(${temp_destination_path}) ) ){
        Move-Item -Path  (ConvertPath(${temp_destination_path})) -Destination ${destination} -Force

        [TimeSpan]$duration=[DateTime]::Now-$start_time
        $script:log.Write( [OutputLogType]::Info, "CopyM2ts_OneFile",( "コピー処理時間 : " + (GetDurationString($duration)) ) )
        
        [string]$destination_file_path = ConvertPath($destination)
        if(-Not(Test-Path($destination_file_path))){
            $script:log.Write( [OutputLogType]::Error, "CopyM2ts_OneFile", ( "コピー失敗 : " + $source_file_name  ) )
        }
    } else {
        $script:log.Write( [OutputLogType]::Error, "CopyM2ts_OneFile", ( "コピー失敗 : " + $source_file_name ) )
    }
}

#########################
# Parameter check
#########################
if( [string]::IsNullOrEmpty("${script:source_root}") -or -Not(Test-Path "${script:source_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "コピー元フォルダがありません '"+${script:source_root}+"'" ) )
    exit
}

if( [string]::IsNullOrEmpty("${script:destination_root}") -or -Not(Test-Path "${script:destination_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "コピー先フォルダがありません '"+${script:destination_root}+"'" ) )
    exit
}

#########################
# Main
#########################
$script:log.Write( [OutputLogType]::Info, "", "【M2TSファイルのコピーを開始します】" )
$script:log.Write( [OutputLogType]::Info, "", "  コピー元フォルダ:${script:source_root}" )
$script:log.Write( [OutputLogType]::Info, "", "  コピー先フォルダ:${script:destination_root}" )

if( CheckAlreadyRunning ){
    $script:log.Write( [OutputLogType]::Info, "", "変換・コピー処理実行中のため中断します" )
    $script:log.Write( [OutputLogType]::Info, "", "【M2TSファイルのコピーを終了しました】" )
    exit
}

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

# Copy M2TS
CopyM2ts -source ${script:source_root} -destination ${script:destination_root}

#shutdown
if( $shutdown ){
    Shutdown($script:log)
}

$script:log.Write( [OutputLogType]::Info, "", "【M2TSファイルのコピーを終了しました】" )
Using Module ".\Utils.psm1"

Param(
    [string]$log_file,
    [string]$source,
    [string]$destination,
    [switch]$nvenc,
    [switch]$quiet,
    [switch]$shutdown,
    [switch]$help
)

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

if([string]::IsNullOrEmpty($log_file)){
    $log_file_path = $script:script_path+((Get-Date).ToString("\\yyyyMMdd_HHmmss_"))+$script_name.Replace(".ps1",".log")
    [Log]$script:log=[Log]::new($script:script_name,$log_file_path)
} else {
    [Log]$script:log=[Log]::new($script:script_name,$log_file)
}

[string]$script:source_root      = $source
[string]$script:destination_root = $destination

[string]$script:ini_file_path    = "${script:script_path}\Convert.ini"
[string]$script:section_convert  = $script:script_name

[bool]$script:quiet_flag=$quiet
[bool]$script:nvenc_flag=$nvenc

[bool]$debug=$false
if($debug){
    # [string]$script:source_root      = "${script:script_path}\files\m2ts"
    # [string]$script:destination_root = "${script:script_path}\files\convert"
    [string]$script:source_root      = "\\bluesky-nas\tv_program_backup\m2ts"
    [string]$script:destination_root = "\\bluesky-nas\tv_program\converted_files"
    $script:quiet_flag = $true

    if( -Not(Test-Path "${script:source_root}") ){
        New-Item -Type Directory ${script:destination_root} > $null
    }
}

[string]$script:amatsukaze_directory = $script:script_path.Replace("\AutomationBatch","")
if( [string]::IsNullOrEmpty($script:amatsukaze_directory) ){
    $script:amatsukaze_directory = ""
}
[string]$script:amatsukaze_cli_exe = "$script:amatsukaze_directory\exe_files\AmatsukazeCLI.exe"
[string]$script:work_directory = "$script:amatsukaze_directory\temp"

[string]$script:convert_to_mp4_commandline = ""
$script:convert_to_mp4_commandline += " -s 1024"
$script:convert_to_mp4_commandline += " --drcs `"$script:amatsukaze_directory\drcs\drcs_map.txt`""
$script:convert_to_mp4_commandline += " -w `"$script:work_directory`""
$script:convert_to_mp4_commandline += " --chapter-exe `"$script:amatsukaze_directory\exe_files\chapter_exe.exe`""
$script:convert_to_mp4_commandline += " --jls `"$script:amatsukaze_directory\exe_files\join_logo_scp.exe`""
$script:convert_to_mp4_commandline += " --cmoutmask 2"
if( $script:nvenc_flag ){
    $script:convert_to_mp4_commandline += " -et NVEnc"
    $script:convert_to_mp4_commandline += " -e `"$script:amatsukaze_directory\exe_files\NVEncC\NVEncC64.exe`""
} else {
    $script:convert_to_mp4_commandline += " -et QSVEnc"
    $script:convert_to_mp4_commandline += " -e `"$script:amatsukaze_directory\exe_files\QSVEncC\QSVEncC64.exe`""
}
$script:convert_to_mp4_commandline += " -j `"$script:work_directory\2-enc.json`""
$script:convert_to_mp4_commandline += " --mp4box `"$script:amatsukaze_directory\exe_files\mp4box.exe`""
$script:convert_to_mp4_commandline += " -t `"$script:amatsukaze_directory\exe_files\timelineeditor.exe`""
if( $script:nvenc_flag ){
    $script:convert_to_mp4_commandline += " -eo `"-c h264 --profile main --vbrhq 0 --vbr-quality 25 --gop-len 90 --cqp 20:23:25`""
} else {
    $script:convert_to_mp4_commandline += " -eo `"-c h264 --profile main --qvbr-quality 25 --gop-len 90 --cqp 20:23:25`""
}
$script:convert_to_mp4_commandline += " -fmt mp4"
$script:convert_to_mp4_commandline += " -m `"$script:amatsukaze_directory\exe_files\muxer.exe`""
$script:convert_to_mp4_commandline += " -bcm 0.5"
$script:convert_to_mp4_commandline += " --chapter"
$script:convert_to_mp4_commandline += " -f `"$script:amatsukaze_directory\avscache\17E37FEB.avs`""
$script:convert_to_mp4_commandline += " --subtitles"
$script:convert_to_mp4_commandline += " --jls-cmd `"$script:amatsukaze_directory\JL\JL_Standard.txt`""
if( $script:nvenc_flag ){
    $script:convert_to_mp4_commandline += " --mpeg2decoder CUVID"
    $script:convert_to_mp4_commandline += " --h264decoder CUVID"
} else {
    $script:convert_to_mp4_commandline += " --mpeg2decoder default"
    $script:convert_to_mp4_commandline += " --h264decoder default"
}
$script:convert_to_mp4_commandline += " --ignore-no-drcsmap"
$script:convert_to_mp4_commandline += " --no-delogo"
$script:convert_to_mp4_commandline += " --ignore-no-logo"
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID171-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1032-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1040-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1040-2.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1048-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1056-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1064-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1072-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID1072-2.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID23608-1.lgd`""
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID101-1.lgd`""#NHK BS
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID211-1.lgd`""#BS11
$script:convert_to_mp4_commandline += " --logo `"$script:amatsukaze_directory\logo\SID171-2.lgd`""#BSテレ東

#########################
# Help
#########################
if( $help ) {
    Write-Host "NAME"
    Write-Host ""
    Write-Host "   $script:script_name - M2TSファイルをCMを抜いたMP4形式に変換する"
    Write-Host ""
    Write-Host "SYPNOSYS"
    Write-Host ""
    Write-Host "   $script:script_name -log_file <file path>"
    Write-Host "                       -source <directory path>"
    Write-Host "                       -destination <directory path>"
    Write-Host "                       [-nvenc] [-quiet] [-shutdown] [-help]"
    Write-Host ""
    Write-Host "DESCRIPTION"
    Write-Host ""
    Write-Host "   -source"
    Write-Host "        変換元のm2tsファイルが格納されているディレクトリを指定する"
    Write-Host ""
    Write-Host "   -destination"
    Write-Host "        mp4ファイルの格納先のディレクトリを指定する"
    Write-Host ""
    Write-Host "   -nvenc"
    Write-Host "        NVEncを使用してエンコードする。指定しない場合はQVEncを使用してエンコードする"
    Write-Host ""
    Write-Host "   -quiet"
    Write-Host "        処理開始前の確認をスキップする"
    Write-Host ""
    Write-Host "   -shutdown"
    Write-Host "        変換終了後、${delay_shutdown}秒後にPCをシャットダウンする"
    Write-Host ""
    Write-Host "   -help"
    Write-Host "        ヘルプを表示する"
    Write-Host ""
    exit
}

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

    [IniFileManager]$ini_file = [IniFileManager]::new($script:ini_file_path)

    $convert_files = @()

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

    foreach( $m2ts_file in $m2ts_files ){

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

            [bool]$convert_flag = $false

            [string]$destination_file_path = $m2ts_file.FullName.Replace($source,$destination).Replace(".m2ts",".mp4")
            $mp4_file = ConvertToMp4_GetMp4File -file_path $destination_file_path -mp4_files $mp4_files

            if( $null -eq $mp4_file ){
                $convert_flag = $true
            } elseif( $m2ts_file.Length -ne $ini_file.GetValueLong( $script:section_convert, $m2ts_file.Name, -1 ) ){
                $convert_flag = $true
            } else {
                [string]$temp_file_path = ( Split-Path -Parent ${destination_file_path}) + "\TEMP_" +(Split-Path -Leaf ${destination_file_path})
                $mp4_file = ConvertToMp4_GetMp4File -file_path $temp_file_path -mp4_files $mp4_files
                if( -Not([string]::IsNullOrEmpty($mp4_file.FullName) ) ){
                    $convert_flag = $true
                }
            }
        }

        if($convert_flag){
            if( $m2ts_file.Length -eq $ini_file.GetValueLong( $script:section_convert, $m2ts_file.Name, -1 ) ){ `
                if( $debug ){ $script:log.Write( [OutputLogType]::Info, "ConvertToMp4", ( "スキップ(変換済み) .... " + $m2ts_file.Name ) ) }
            } else {

                [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) )
        }
        $console_output += "`n  変換ファイル格納先:$destination"
        $script:log.Write( [OutputLogType]::Info, "ConvertToMp4", $console_output )

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

    foreach( $convert_file in $convert_files ){

        $destination_mp4 = $convert_file.FullName.Replace($source,$destination).Replace(".m2ts",".mp4")
        ConvertToMp4_OneFile -source $convert_file.FullName -destination $destination_mp4

        if( Test-Path( ConvertPath($destination_mp4) ) ){
            $m2ts_file_convert = (Get-Item -Path ( ConvertPath($convert_file.FullName) ) )
            $ini_file.Update( $script:section_convert, $m2ts_file_convert.Name, $m2ts_file_convert.Length )
            $ini_file.Write()
        }
    }

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

function ConvertToMp4_GetMp4File {
    Param(
        [string]$file_path,
        [System.Array]$mp4_files
    )

    foreach( $mp4_file in $mp4_files ){
        if( $file_path.Equals($mp4_file.FullName) ){
            return $mp4_file
        }
    }

    return $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
    }

    $script:log.Write( [OutputLogType]::Info, "ConvertToMp4_OneFile", "Convert `"$source_file_name`" ..." )

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

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

    [string]$argument_list = $script:convert_to_mp4_commandline
    $argument_list += " --input `"${source}`" "
    $argument_list += " --output `"${temp_file}`" "
    $script:log.Write( [OutputLogType]::Info, "ConvertToMp4_OneFile", "Start-Process $script:amatsukaze_cli_exe -ArgumentList `"$argument_list`" -Wait" )

    [System.Diagnostics.ProcessStartInfo]$process_start_info = New-Object System.Diagnostics.ProcessStartInfo
    $process_start_info.FileName = $script:amatsukaze_cli_exe
    $process_start_info.CreateNoWindow = $false
    $process_start_info.RedirectStandardError = $false
    $process_start_info.RedirectStandardOutput = $true
    $process_start_info.UseShellExecute = $false
    $process_start_info.Arguments = $argument_list

    [System.Diagnostics.Process]$process = New-Object System.Diagnostics.Process
    $process.StartInfo = $process_start_info
    $string_builder = New-Object -TypeName System.Text.StringBuilder
    $output_data_received = {
        if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
            $Event.MessageData.AppendLine($EventArgs.Data)
        }
    }
    $output_event = Register-ObjectEvent -InputObject $process `
        -Action $output_data_received -EventName 'OutputDataReceived' `
        -MessageData $string_builder
    [void]$process.Start()
    $process.BeginOutputReadLine()
    $process.WaitForExit()
    Unregister-Event -SourceIdentifier $output_event.Name
    $string_builder.ToString()

    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"))
    }

    if( Test-Path(ConvertPath(${temp_file}) ) ){
        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
        $script:log.Write( [OutputLogType]::Info, "ConvertToMp4_OneFile", ( "変換処理時間 : " + (GetDurationString($duration_convert) ) ) )
        
        [string]$destination_file_path = ConvertPath($destination)
        if(-Not(Test-Path($destination_file_path))){
            $script:log.Write( [OutputLogType]::Error, "ConvertToMp4_OneFile", ( "変換失敗 : " + $source_file_name ) )
        }
    } else {
        $script:log.Write( [OutputLogType]::Error, "ConvertToMp4_OneFile", ( "変換失敗 : " + $source_file_name ) )
    }
}

#########################
# Parameter check
#########################
if( [string]::IsNullOrEmpty("${script:source_root}") -or -Not(Test-Path "${script:source_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "変換元フォルダがありません '"+${script:source_root}+"'" ) )
    exit
}

if( [string]::IsNullOrEmpty("${script:destination_root}") -or -Not(Test-Path "${script:destination_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "変換先フォルダがありません '"+${script:destination_root}+"'" ) )
    exit
}

if( -Not(Test-Path "${script:work_directory}") ){
    New-Item -Type Directory ${script:work_directory} > $null
    if( -Not(Test-Path "${script:work_directory}") ){
        $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "作業用ディレクトリを作成できませんでした '"+${script:work_directory}+"'") )
        exit
    }
}

#########################
# Main
#########################
$script:log.Write( [OutputLogType]::Info, "", "【M2TS->MP4変換を開始します】" )
$script:log.Write( [OutputLogType]::Info, "", "  変換元フォルダ:$script:source_root" )
$script:log.Write( [OutputLogType]::Info, "", "  変換先フォルダ:$script:destination_root" )

if( !$debug -and ( CheckAlreadyRunning ) ){
    $script:log.Write( [OutputLogType]::Info, "", "変換・コピー処理実行中のため中断します" )
    $script:log.Write( [OutputLogType]::Info, "", "【M2TS->MP4変換を終了しました】" )
    exit
}

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

if( -Not(Test-Path "${script:destination_root}") ){
    New-Item -Type Directory ${script:destination_root} > $null
}
# Convert to MP4
ConvertToMp4 -source "$script:source_root" -destination "$script:destination_root"

#shutdown
if( $shutdown ){
    Shutdown($script:log)
}

$script:log.Write( [OutputLogType]::Info, "", "【M2TS->MP4変換を終了しました】" )
Using Module ".\Utils.psm1"

Param(
    [string]$log_file,
    [string]$source,
    [int]$day,
    [float]$totalsize,
    [switch]$quiet,
    [switch]$shutdown,
    [switch]$help
)

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

if([string]::IsNullOrEmpty($log_file)){
    $log_file_path = $script:script_path+((Get-Date).ToString("\\yyyyMMdd_HHmmss_"))+$script_name.Replace(".ps1",".log")
    [Log]$script:log=[Log]::new($script:script_name,$log_file_path)
} else {
    [Log]$script:log=[Log]::new($script:script_name,$log_file)
}

[string]$script:source_root    = $source
[string]$script:duration_day   = $day
[uint64]$script:totalsize_byte = $totalsize*1024*1024*1024

[bool]$script:quiet_flag=$quiet

[bool]$debug=$false
if($debug){
    [string]$script:source_root    = "${script:script_path}\files\convert_backup"
    [int]$script:duration_day      = 100
    [uint64]$script:totalsize_byte = 10*1024*1024*1024
}

#########################
# Help
#########################
if( $help ) {
    Write-Host "NAME"
    Write-Host ""
    Write-Host "   $script:script_name - 最終更新日から指定した日数を経過したファイルを削除し、"
    Write-Host "                    フォルダサイズが指定サイズ以下になるように古いファイルを削除する"
    Write-Host ""
    Write-Host "SYPNOSYS"
    Write-Host ""
    Write-Host "   $script:script_name -source <directory path>"
    Write-Host "                       [-day <number of day>]"
    Write-Host "                       [-totalsize <number of day>]"
    Write-Host "                       -log_file <file path>"
    Write-Host "                       [-quiet] [-help]"
    Write-Host ""
    Write-Host "DESCRIPTION"
    Write-Host ""
    Write-Host "   -source"
    Write-Host "        ファイル削除対象のディレクトリを指定する"
    Write-Host ""
    Write-Host "   -day"
    Write-Host "        最終更新日からここで指定した日数を経過したファイルを削除する"
    Write-Host ""
    Write-Host "   -totalsize"
    Write-Host "        フォルダ内のファイルが占有しているファイルサイズが指定したサイズ(単位:GB)以上の場合、”
    Write-Host "        指定サイズ以下になるように最終更新日時の古いものを削除する"
    Write-Host ""
    Write-Host "   -log_file"
    Write-Host "        ログファイルパスを指定する"
    Write-Host ""
    Write-Host "   -quiet"
    Write-Host "        処理開始前の確認をスキップします"
    Write-Host ""
    Write-Host "   -shutdown"
    Write-Host "        コピー終了後、${delay_shutdown}秒後にPCをシャットダウンする"
    Write-Host ""
    Write-Host "   -help"
    Write-Host "        ヘルプを表示する"
    Write-Host ""
    exit
}

###################
# Function
###################
function DeleteFiles_Day {
    Param(
        [string]$source,
        [int]$day
    )

    $files = Get-ChildItem "$source" -Recurse

    $delete_files = @()
    foreach( $file in $files ){
        if( !$file.Name.StartsWith("_") -and !$file.Name.StartsWith(".") ){
            [timespan]$time_diff = [DateTime]::Now - $file.LastWriteTime
            if( $time_diff.TotalDays -gt $day ){
                $delete_files += $file
            }
        }
    }

    if( $delete_files.Count -le 0 ){
        [string]$console_output = "最終書込日時から${day}日経過したファイルはありませんでした"
        $script:log.Write( [OutputLogType]::Info, "DeleteFiles_Day", $console_output )
    } else {
        [string]$console_output = "最終書込日時から${day}日経過した以下のファイルを削除します"
        foreach( $delete_file in $delete_files ){
            $console_output += ( "`n  - " + (Split-Path -Leaf $delete_file.Name) `
                            + " (最終書込日時:" +  $delete_file.LastWriteTime.ToString("yyyy/MM/dd") + " / " `
                            + "ファイルサイズ:" +  ( [Math]::Truncate( ( ( $delete_file.Length ) / 1024 / 1024 / 1024 ) * 100 ) / 100 ) + "GB )" )
        }
        $script:log.Write( [OutputLogType]::Info, "DeleteFiles_Day", $console_output )

        if( !$script:quiet_flag ){
            $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))
    }
}

function DeleteFiles_TotalSize{
    Param(
        [string]$source,
        [uint64]$totalsize
    )

    $files = ( Get-ChildItem "$source" -Recurse ) | Sort-Object -Property LastWriteTime -Descending

    $delete_files = @()
    [uint64]$current_size = 0
    foreach( $file in $files ){
        $current_size+=$file.Length
        if( $file.Name.EndsWith(".m2ts") -or $file.Name.EndsWith(".mp4") -or $file.Name.EndsWith(".ass") ){
            if( $current_size -gt $totalsize ){
                $delete_files += $file
            }
        }
    }

    [float]$current_size_gb=[Math]::Truncate( ( ( $current_size ) / 1024 / 1024 / 1024 ) * 10 ) / 10

    if( $delete_files.Count -le 0 ){
        [string]$console_output = "現在のフォルダサイズは${current_size_gb}GBのため、削除するファイルはありませんでした"
        $script:log.Write( [OutputLogType]::Info, "DeleteFiles_TotalSize", $console_output )
    } else {
        [string]$console_output = "現在のフォルダサイズが${current_size_gb}GBのため、以下のファイルを削除します"
        foreach( $delete_file in $delete_files ){
            $console_output += ( "`n  - " + (Split-Path -Leaf $delete_file.Name) `
                            + " (最終書込日時:" +  $delete_file.LastWriteTime.ToString("yyyy/MM/dd") + " / " `
                            + "ファイルサイズ:" +  ( [Math]::Truncate( ( ( $delete_file.Length ) / 1024 / 1024 / 1024 ) * 100 ) / 100 ) + "GB )" )
        }
        $script:log.Write( [OutputLogType]::Info, "DeleteFiles_TotalSize", $console_output )

        if( !$script:quiet_flag ){
            $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.FullName))
    }
}

function DeleteFolders {
    Param(
        [string]$source
    )

    $folders = (Get-ChildItem $source -Directory -Recurse -ErrorAction Stop) | Sort-Object -Descending

    $delete_folders = @()
    foreach($folder in $folders){
        if( $folder.GetFileSystemInfos().Count -eq 0 ) {
            $delete_folders += $folder
        }
    }

    if( $delete_folders.Count -gt 0 ){
        [string]$console_output = "以下の空フォルダを削除します"
        foreach( $delete_folder in $delete_folders ){
            $console_output += ( "`n  - " + (Split-Path -Leaf $delete_folder) )
        }
        $script:log.Write( [OutputLogType]::Info, "DeleteFolders", $console_output )

        if( !$script:quiet_flag ){
            $console_output = "よろしいですか?([y]es/[N]o)"

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

    foreach($delete_folder in $delete_folders){
        Remove-Item  $delete_folder.FullName -Recurse -Force
    }
}

function DeleteUnnecessaryFiles {
    Param(
        [string]$source
    )

    $files = Get-ChildItem "$source" -Recurse -include '._*' -Force

    if( $files.Count -gt 0 ){
        [string]$console_output = "以下の不要ファイルを削除します。"
        foreach( $file in $files ){
            $console_output += ( "`n  - " + (Split-Path -Leaf $file) )
        }
        $script:log.Write( [OutputLogType]::Info, "DeleteUnnecessaryFiles", $console_output )

        if( !$script:quiet_flag ){
            $console_output = "よろしいですか?([y]es/[N]o)"

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

    foreach($file in $files){
        $script:log.Write( [OutputLogType]::Info, "DeleteUnnecessaryFiles", ( "Delete '"+$file.Name+"'") )
        Remove-Item -Path (ConvertPath($file.FullName)) -Force
    }
}

#########################
# Parameter check
#########################
if( [string]::IsNullOrEmpty("${script:source_root}") -or -Not(Test-Path "${script:source_root}") ){
    $script:log.Write( [OutputLogType]::Error, "ParameterCheck", ( "ファイル削除対象フォルダがありません '"+${script:source_root}+"'") )
    exit
}

#########################
# Main
#########################
$script:log.Write( [OutputLogType]::Info, "", "【ファイル・空フォルダ削除を開始します】" )
$script:log.Write( [OutputLogType]::Info, "", "  ファイル・空フォルダ削除対象フォルダ:$script:source_root" )

if( !$debug -and ( CheckAlreadyRunning ) ){
    $script:log.Write( [OutputLogType]::Info, "", "変換・コピー処理実行中のため中断します" )
    $script:log.Write( [OutputLogType]::Info, "", "【ファイル・空フォルダ削除を終了しました】" )
    exit
}

if( ( ${script:duration_day} -le 0 ) -and ( ${script:totalsize_byte} -le 0 ) ){
    $script:log.Write( [OutputLogType]::Info, "", "  不要ファイル・空フォルダを削除します" )
} else {
    if( ${script:duration_day} -gt 0 ){
        $script:log.Write( [OutputLogType]::Info, "", "  削除対象ファイル       :最終書込日時から${script:duration_day}日経過したファイル" )
    }
    if( ${script:totalsize_byte} -gt 0 ){
        $script:log.Write( [OutputLogType]::Info, "", ( "  フォルダサイズ         :"+(${script:totalsize_byte}/1024/1024/1024)+"GB以下" ) )
    }
}

# delete
if( ${script:duration_day} -gt 0 ){
    DeleteFiles_Day -source "$script:source_root" -day ${script:duration_day}
}
if( ${script:totalsize_byte} -gt 0 ){
    DeleteFiles_TotalSize -source "$script:source_root" -totalsize ${script:totalsize_byte}
}
DeleteFolders -source "$script:source_root"
DeleteUnnecessaryFiles -source "$script:source_root"

#shutdown
if( $shutdown ){
    Shutdown($script:log)
}

$script:log.Write( [OutputLogType]::Info, "", "【ファイル・空フォルダ削除を終了しました】" )