Thursday, 20 October 2011

Using Powershell V3 to control utorrent via Webui and JSON

Script purpose
Create a powershell script that could change utorrent settings via the webUI interface.

Points
The Utorrent webUI communicates across HTML using JSON.
Powershell V3 preview has been released and it contains new functions to allow you to work with JSON objects.
Remember to check that the webUI is enabled in the utorrent application.

To be able to communicate with uTorrent you first need to understand the token authentication process. This involves authenticating with the application and then attaching a token to all future communications. All data returned from utorrent is in JSON format and can be parsed using the new powershell V3 function ConvertFrom-Json.

The resulting script can be use the adjust any of the utorrent setting. The uTorrent webUI API link below detials all the differnt commands.

Example execution
To ensure that the scheduler is enabled (I often disable it and forget to re enable it) I run the example below every day at 1 am. It sets the scheduler to be on.

#Enable utorrent scheduler
Utorrent-SetSettings "sched_enable" 1

Script
$Script:Version      = '0.1.0.1'
#########################################################
#Load config from file
#########################################################
[String]$Script:Server = 'servername'
[String]$Script:Port = 'port'
[String]$Script:User = 'utorrent user name'
[String]$Script:Pass = 'pass'

[String]$Script:UtorrentUrl = "http://$server`:$port/gui/"
[String]$Script:token = ""
$Script:webClient = $null

function Utorrent-HttpGet([string]$Comand)
{
    if ([string]::IsNullOrEmpty($Script:token) -eq $true -or $Script:webClient -eq $null) 
    {
        $Script:webClient = new-object System.Net.WebClient
        $Script:webClient.Headers.Add("user-agent", "PowerShell Script")
    
        if ([string]::IsNullOrEmpty($Script:User) -eq $false) 
        {
            $Script:webClient.Credentials = new-object System.Net.NetworkCredential($Script:User, $Script:Pass)
        }

        $responce = $Script:webClient.DownloadString($Script:UtorrentUrl + "token.html")
        [string]$cookies =  $Script:webClient.ResponseHeaders["Set-Cookie"]

        if ($responce -match ".*<div[^>]*id=[`"`']token[`"`'][^>]*>([^<]*)</div>.*")
        {
            $Script:token = $matches[1]
            $Script:webClient.Headers.Add("Cookie", $cookies)
     }
    }
    $url = "$($UtorrentUrl)?$($Comand)&token=$($Script:token)"
    Write-Host ("Calling url`t$url")
    $response = $Script:webClient.DownloadString($url)
    $json = ConvertFrom-JSON $response
    if($json.build -ne $null)
    {
        Write-Host ("Success $($json.build)")
    }
    return $json
}

function Utorrent-GetList() 
{
    Write-verbose ("$($MyInvocation.MyCommand.Name) v$Version")
 $dict = @{};
    $json = Utorrent-HttpGet "list=1"
    $json.torrents | Foreach-Object {
        $dict.add($_[2],$_)
    } 
    return $dict 
}

function Utorrent-GetSettings() 
{
 Write-verbose ("$($MyInvocation.MyCommand.Name) v$Version")
 $dict = @{};
    $json = Utorrent-HttpGet "action=getsettings"
    $json.settings | Foreach-Object {
        $dict.add($_[0],$_)
    } 
    $dict
}

function Utorrent-SetSettings([string]$setting, [string]$value) 
{
 Write-verbose ("$($MyInvocation.MyCommand.Name) v$Version")
    $json = Utorrent-HttpGet "action=setsetting&s=$setting&v=$value"
}

function Utorrent-GetTorrentFiles([string]$torrentHash) 
{
 Write-verbose ("$($MyInvocation.MyCommand.Name) v$Version")
 $dict = @{};
    $json = Utorrent-HttpGet "action=getfiles&hash=$torrentHash"
    $json.files | Foreach-Object {
        $dict.add($_[0],$_)
    } 
    $dict
}

function Utorrent-GetTorrentProps([string]$torrentHash) 
{
 Write-verbose ("$($MyInvocation.MyCommand.Name) v$Version")
 $dict = @{};
    $json = Utorrent-HttpGet "action=getprops&hash=$torrentHash"
    $json.props | Foreach-Object {
        $dict.add($_[0],$_)
    } 
    $dict
}

function Utorrent-SetTorrentProps([string]$torrentHash, [string]$property, [string]$value) 
{
 Write-verbose ("$($MyInvocation.MyCommand.Name) v$Version")
 $json = Utorrent-HttpGet "action=setprops&hash=$torrentHash&s=$property&v=$value"
}

function Utorrent-ParseSettings([string]$json) 
{
 if ([string]::IsNullOrEmpty($json) -eq $false -and $jayson[0] -eq "{")
    {
        return ConvertFrom-Json $json
    }
}

Tuesday, 2 August 2011

Powershell utorrent

Script purpose
When a torrent finishes copy the downloaded files to a new folder for processing.

Script notes
Utorrent has a new feature allowing a command line to be executed when a torrent changes state or finishes. I wanted to test this out and see if I could perform some utorrent automation via powershell.
The desired process is. One a torrent finishes downloading it will move in to seeding. The downloaded files will be copied from the seeding folder to a "new downloads" folder where they can be processed (uncompressing, moving, renaming and so on) without having to stop the torrent from seeding.
When the torrent finishes seeding the files in the seeding folder can be deleted.

To achieve this I have utorrent copy the the downloaded files to a new folder. When I finish seeding the torrent I can remove the files in the seeding folder. I also have another script that runs each day to clean up the seeding folder in case any torrents are removed from utorrent without removing the datafiles.

One tricky part in achieving this was that folder and file torrents are treated differently. There is no single path the the item I want to copy. From testing I came to If the torrent name ($N) is equal to file name ($F) its a file torrent and the file path is D$\$F (file $F in folder path $D). Otherwise we can just use the folder path ($D). I added a check to see if the folder name in $D was the same as the torrent name ($N) to catch any issues for folder torrents, this might not be needed.

The time take to perform the copy and the size of the folders before and after are logged to a file. This was so I could confirm that the copy was successful (this a POC script so testing and logs are needed).

param(
    [string]$Reason = "Unknown", 
    [string]$F = "", 
    [string]$D = "",
    [string]$N = "",
    [string]$S = "",
    [string]$P = "",
    [string]$L = "",
    [string]$T = "",
    [string]$M = "",
    [string]$I = ""
    )
 
#*******************************************************************
# Global Variables
#*******************************************************************
$Script:Version      = '0.4.0.5'
<#
Comments
       copys completed torrents

Notes
    Scheduled Performs (via another script)
        Check for removed seeds
        Check for dead partial
  
  Inputs to this scripts
    $Reason - Default = 'unknown' - Does nothing
              Completed - The torrent was completed.
              StateChanged - The torrent state changed.

  Utorrent Falcon Commands
    %F - Name of downloaded file (for single file torrents)
    %D - Directory where files are saved
    %N - Title of torrent
    %S - State of torrent
    %P - Previous state of torrent
    %L- Label
    %T-Tracker
    %M - Status message string (same as status column)
    %I- hexencoded info-hash
  State is one of:
    error = 1, 
    checked = 2
    paused = 3
    super seeding = 4
    seeding = 5
    downloading =6
    super seed [F] = 7
    seeding [F] = 8
    downloading [F] = 9
    queued seed = 10,
    finished = 11
    queued = 12
    stopped = 13

  How to call from utorrent
    called from Completed
      powershell.exe -noprofile "& 'c:\path\utorrent.ps1' -Reason 'Completed' -F '%F' -D '%D' -N '%N' -S '%S' -P '%P' -L '%L' -T '%T' -M '%M' -I '%I'"

    called from StateChange
      powershell.exe -noprofile "& 'c:\path\utorrent.ps1' -Reason 'StateChanged' -F '%F' -D '%D' -N '%N' -S '%S' -P '%P' -L '%L' -T '%T' -M '%M' -I '%I'"
#>

#########################################################
#Load config from file
#########################################################
[xml]$configFile = get-ScriptConfig
if ($configFile -eq $null) {
   Write-Error "Failed to load config`nExiting"
 Exit}

#Location where logs will be written
$GlobalLogPath = $configFile.Configuration.GlobalLogPath 

#Location of Utorrent completed folders (Active torrents)
$UtorrentCompletedFolder = $configFile.Configuration.UtorrentCompletedFolder 

#Where to place new unsorted downloads
$DownloadsCompletedFolder = $configFile.Configuration.DownloadsCompletedFolder 

###########################################################
#functions
###########################################################
function GetState( [string] $intState ) {
    #converts the integer state value to its English name
    switch ($intState)
    {
        "1" {return 'error'}
        "2" {return 'checked'}
        "3" {return 'paused'}
        "4" {return 'super seeding'}
        "5" {return 'seeding'}
        "6" {return 'downloading'}
        "7" {return 'super seed [F]'}
        "8" {return 'seeding [F]'}
        "9" {return 'downloading [F]'}
        "10" {return 'queued seed'}
        "11" {return 'finished'}
        "12" {return 'queued'}
        "13" {return 'stopped'}
        default {return 'unknown'}
    }
}

#creates a dir it is needed
function CreateDirectoryIfNeeded ( [string] $directory ) {

    if ((test-path -LiteralPath $directory) -ne $True)
    {
        
        New-Item $directory -type directory | out-null
        
        if ((test-path -LiteralPath $directory) -ne $True)
        {
            return #[boolean]$false
        }
        else
        {
            return #[boolean]$true
        }
    }
    else
    {
        return #[boolean]$true
    }
}

#logs a message to file,
#the filename changes every month to keep the file in a usablestate
function Log_Message([string]$Message) {
    [datetime]$date = get-date
    [String]$filename = "$GlobalLogPath\uTorrent-{1}.log" -f $functionName, $date.ToString( "yyyyMM")
    
    CreateDirectoryIfNeeded $GlobalLogPath
    
    #Writes message to log file
    add-content $filename "$date | $Message"
    
    #displays message on console
    Write-Host $Message
}

function Get-FolderSize([string]$FolderPath) {
<#
    .Synopsis
        gets the folder size recursive
    .Example
        [long]$result = Get-FolderSize "C:\temp"
#>
    [long]$FolderLength = (Get-ChildItem -LiteralPath $FolderPath -Recurse | Measure-Object -Property Length -Sum).Sum
    return $FolderLength 
}

function Get-BytesasString([long]$Bytes) {
<#
    .Synopsis
        Displays the folder size in a pretty way
    .INPUTS
        None. You cannot pipe objects to Get-BytesasString.
#>
    if ($Bytes -gt 1073741823)
    {
        [Decimal]$size = $Bytes / 1073741824
        return "{0:##.##} GB" -f $size 
    }
    elseif ($Bytes -gt 1048575)
    {
        [Decimal]$size = $Bytes / 1048576
        return "{0:##.##} MB" -f $size
    }
    elseif ($Bytes  -gt 1023)
    {
        [Decimal]$size  = $Bytes / 1024
        return "{0:##.##} KB" -f $size
    }
    elseif ($Bytes -gt 0)
    {
        [Decimal]$size = $Bytes
        return "{0:##.##} bytes" -f $size
    }
    else
    {
        return "0 bytes";
    }

}

###########################################################
#Main script
###########################################################

#Log Utorrent messages
Log_Message "$Reason | F=$F|D=$D|N=$N|S=$(GetState $S)|P=$(GetState $P)|L=$L|T=$T|M=$M|I=$I|END"

#tasks
#Process completed torrent
if($Reason -eq 'Completed')
{
 $Source = $null
    #if the name is equal to the file its prob single file torrent
    if($N -eq $F)
    {
        #It's a single file torrent
        $Source = join-path -path $D -childpath $F
    }
    else
    {
        #It's a folder torrent
        $Source = $D
        $sourceObj = get-Item -LiteralPath $Source
        if ($sourceObj.name -ne $N) 
        {
            Log_Message "ERROR| Folder name does not match %N: $($sourceObj.name)"
            $Source = $null
        } 
    }
 if ((test-path -LiteralPath $Source) -eq $False) 
    {
        Log_Message "ERROR | File does not exist: $Source. Exiting"
    } 
    else
    { 
        $message  = ""
        $measure = Measure-Command { 
            [string]$sizeSourceBefore = Get-BytesasString (Get-FolderSize $Source)    
            
            Log_Message "Torrent Completed, copying object '$Source' to '$DownloadsCompletedFolder'"
            Copy-Item -LiteralPath $Source -Destination $DownloadsCompletedFolder -Recurse -Force
            
            $sourceObj = get-Item -LiteralPath $Source
            $targetFolder = join-path -path $DownloadsCompletedFolder -childpath $sourceObj.name 
            [string]$sizeTargetAfter = Get-BytesasString (Get-FolderSize $targetFolder)    
            
            [string]$sizeSourceAfter = Get-BytesasString (Get-FolderSize $Source)
            
            #log the before and after sizes of the source and destination folders, for debug
            $message = "sizeSourceBefore $sizeSourceBefore, sizeSourceAfter $sizeSourceAfter, sizeTarget $sizeTargetAfter"
        }
        
        #log how long the copy took
        Log_Message "$message - In $($measure.Days) Days $($measure.Hours):$($measure.Minutes):$($measure.Seconds).$($measure.Milliseconds)" 
        
    }
}

Happy to receive feedback

Powershell Find Rar files

Script purpose
Find all rar files in a folder and child folders. Then use the Extract-RAR-File function to decompress and remove the rar files.

Script notes
The rar files are matched using the regular expression ".*(?:(?<!\.part\d\d\d|\.part\d\d|\.part\d)\.rar|\.part0*1\.rar)". This could be improved as it matches all rar files and not just the first one is a set. I forget where found this regexp, Thanks to those who wrote it.
I excluded rar files in folders named trash as these have already been extracted.

function Extract-RARs-in-Folder([string]$Folder) {
<#
    .Synopsis
        Extracts all the rar files in the folder
    .Description
    .Example
        Extract-RARs-in-Folder C:\temp\
        would extract C:\temp\foo.rar and C:\temp\bar.rar
    .Parameter Folder
        Path of folder to be processed 
  .Link
        http://heazlewood.blogspot.com
#>
    #reference to source folder
    $basketFolder = get-item -LiteralPath $Folder   

    #Search the basket folder for any rar files
    $rarFiles = get-Childitem -LiteralPath $basketFolder.FullName -recurse |  
    where{$_.Name -match ".*(?:(?<!\.part\d\d\d|\.part\d\d|\.part\d)\.rar|\.part0*1\.rar)"} 
        
    foreach ($rarfile in $rarFiles)
    { 
        #if none are found an empty object is returned filter these by checking if the object exists
        if ($rarfile.Exists -eq $true)
        {
            write-verbose "processing matched rar : $($rarfile.Name)"
            if ($rarfile.Directory.Name -ne "Trash" )
            { 
                #this is a hack needed because I remove or move rar files in the Extract-RAR-File function.
                #regexp should be updated to only match on the first file of a span so this is not needed
                if (Test-Path $rarfile.Fullname  ) 
                {
                    $result = Extract-RAR-File $rarfile.FullName $true
                }
            }
            else
            {
                write-verbose "skipping item due to folder name$($_.FullName)"
            }
        }
    }
}

Powershell Folder Size

Powershell Folder Size

Script purpose
Calculate the size in bytes of a folder.

Script notes
I have some scripts that move folders from one drive to another or one pc to another. I use this to log size of folders that I move, before and after the move is completed.
The second script displays the bytes in a more user/log friendly manner.

function Get-FolderSize([string]$FolderPath) {
<#
    .Synopsis
        gets the folder size recursive
    .Example
        [long]$result = Get-FolderSize "C:\temp"
    .Parameter Source
    .Link
        http://heazlewood.blogspot.com/
#>
    [long]$FolderLength = (Get-ChildItem -LiteralPath $FolderPath -Recurse | Measure-Object -Property Length -Sum).Sum
    return $FolderLength 
}

#
function Get-BytesasString([long]$Bytes) {
<#
    .Synopsis
        Displays the bytes in a pretty way
    .Link
        http://heazlewood.blogspot.com/
#>
    if ($Bytes -gt 1073741823)
    {
        [Decimal]$size = $Bytes / 1073741824
        return "{0:##.##} GB" -f $size 
    }
    elseif ($Bytes -gt 1048575)
    {
        [Decimal]$size = $Bytes / 1048576
        return "{0:##.##} MB" -f $size
    }
    elseif ($Bytes  -gt 1023)
    {
        [Decimal]$size  = $Bytes / 1024
        return "{0:##.##} KB" -f $size
    }
    elseif ($Bytes -gt 0)
    {
        [Decimal]$size = $Bytes
        return "{0:##.##} bytes" -f $size
    }
    else
    {
        return "0 bytes";
    }
}

Powershell backup folder

Script purpose
Create a copy of a folder appending the current date and time to the newly created folder.

Script notes
Script is straight forward. A new folder (named FolderName-backupyyyyMMdd-hhmm) is created in the same path as the source folder. All the files are copied from the source to the new folder.

I use this to create a backup of my scripts on my dev pc before I sign and deploy them.

function Backup-Folder([string]$FolderPath)
{
<#
    .Synopsis
        Creates a back up copy of a folder
    .Description
    .Example
        Backup-Folder "C:\temp\folder1"
        Creates a folder "C:\temp\folder1-backupyyyyMMdd-hhmm"
    .Parameter FolderPath
        Path of folder that will be backed up
    .Link
        http://heazlewood.blogspot.com/
#>
  if (test-path -LiteralPath $FolderPath) {
 $FolderToCopy = Get-Item $FolderPath
 $newPath = Join-Path "$($FolderToCopy.Parent.FullName)" "$($FolderToCopy.Name)-backup_$((get-date).toString('yyyyMMdd-hhmm'))"
    
 Write-Host "copy $($FolderToCopy.FullName) to $newPath"
    
 copy -LiteralPath $FolderPath -Destination "$newPath" -Recurse -Force
  }
}

Monday, 1 August 2011

Powershell unrar

Script purpose
Uncompress a rar or set of rar files using unrar.exe. Once successfully completed remove the original rar files.

Script Notes
Files are extracted to the same folder as the rar file.

Success is determined by checking the output of the unrar executable. If it is successful there should be the text 'All OK' in the output. Files are only removed if this text is found on a single line by itself.

Rar files to be deleted are extracted from the output of the unrar executable.

The RemoveSuccessful parameter was added for testing. If set to false rar files will be moved to sub folder called trash, otherwise they are deleted.

Script
$Script:unrarName =  "path to unrar.exe"
 
function Extract-RAR-File([string]$FilePath, [bool]$RemoveSuccessful= $false) 
{
<#
    .Synopsis
        unrars a file or set of rar files, then if "all ok" 
        removes or moves the original rar files
    .Example
        Extract-RAR-File c:\temp\foo.rar
        Extracts contents of foo.rar to folder temp.
    .Parameter FilePath
        path to rar file 
    .Parameter RemoveSuccessful
        remove rar files if successful otherwise move files to folder called trash
    .Link
        http://heazlewood.blogspot.com
#>
    
    # Verify we can access UNRAR.EXE .
 if ([string]::IsNullOrEmpty($unrarName) -or (Test-Path -LiteralPath $unrarName) -ne $true)
 {
     Write-Error "Unrar.exe path does not exist '$unrarPath'."
        return
    }
 
    [string]$unrarPath = $(Get-Command $unrarName).Definition
    if ( $unrarPath.Length -eq 0 )
    {
        Write-Error "Unable to access unrar.exe at location '$unrarPath'."
        return
    }

   # Verify we can access to the compressed file.
 if ([string]::IsNullOrEmpty($FilePath) -or (Test-Path -LiteralPath $FilePath) -ne $true)
 {
     Write-Error "Compressed file does not exist '$FilePath'."
        return
    }
 
    [System.IO.FileInfo]$Compressedfile = get-item -LiteralPath $FilePath 
    
    #set Destination to basepath folder
    #$fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Compressedfile.Name)
    #$DestinationFolder = join-path -path $Compressedfile.DirectoryName -childpath $fileBaseName
    
    #set Destination to parent folder
    $DestinationFolder = $Compressedfile.DirectoryName 

    # If the extract directory does not exist, create it.
    CreateDirectoryIfNeeded ( $DestinationFolder ) | out-null

    Write-Output "Extracting files into $DestinationFolder"
    &$unrarPath x -y  $FilePath $DestinationFolder | tee-object -variable unrarOutput 
    
    #display the output of the rar process as verbose
    $unrarOutput | ForEach-Object {Write-Verbose $_ }
     
    if ( $LASTEXITCODE -ne 0 )
    { 
        # There was a problem extracting. 
        #Display error
        Write-Error "Error extracting the .RAR file" 
    }
    else
    {
        # check $unrarOutput to remove files
        Write-Verbose "Checking output for OK tag"  
        if ($unrarOutput -match "^All OK$" -ne $null) {
            if ($RemoveSuccessful) {
                Write-Verbose "Removing files"  
                
                #remove rar files listed in output.
                $unrarOutput -match "(?<=Extracting\sfrom\s)(?<rarfile>.*)$" | 
                ForEach-Object {$_ -replace 'Extracting from ', ''} | 
                ForEach-Object { get-item -LiteralPath $_ } | 
                remove-item
                
            } else {
                Write-Verbose "Moving files to trash folder`n$trashPath"  
        
                [string]$trashPath = join-path -path $DestinationFolder "Trash"
                
                #create trash folder to move rars to
                CreateDirectoryIfNeeded ($trashPath)
                
                #move rar files listed in output.
                $unrarOutput -match "(?<=Extracting\sfrom\s)(?<rarfile>.*)$" | 
                ForEach-Object {$_ -replace 'Extracting from ', ''} | 
                foreach-object { get-item -LiteralPath $_ } | 
                move-item -destination $trashPath
            }
        }
    }
}

function CreateDirectoryIfNeeded ( [string] $Directory ){
<#
    .Synopsis
        checks if a folder exists, if it does not it is created
    .Example
        CreateDirectoryIfNeeded "c:\foobar"
        Creates folder foobar in c:\
    .Link
        http://heazlewood.blogspot.com
#>
    if ((test-path -LiteralPath $Directory) -ne $True)
    {
        New-Item $Directory-type directory | out-null
        
        if ((test-path -LiteralPath $Directory) -ne $True)
        {
            Write-error ("Directory creation failed")
        }
        else
        {
            Write-verbose ("Creation of directory succeeded")
        }
    }
    else
    {
        Write-verbose ("Creation of directory not needed")
    }
}

I have used this quite a bit, on various different files successfully but try with parameter RemoveSuccessful = $false first (its sure to have bugs). If you have any suggestions please share.

Links
Unrar download page http://www.rarlab.com/rar_add.htm

Chris