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

7 comments:

  1. I'm calling a PS script to run when a download finishes. It emails me to tell me a download has finished. So, far, all I've been able to do is make a script that will do a search in the Utorrent folder, and it will tell me the name of the last file created. This way I can see in my email what file just download. But this won't work properly when I'm using rss feeds that will download multiple files. I'm trying to figure out if I can use the Utorrent parameters i.e, %F,. Ideally, I'm trying to make the script email me when a torrent has finished and tell me what was just download.

    $message=new-object Net.Mail.MailMessage('me@gmail.com', 'me@gmail.com', 'Download Has Finished', 'Message Body')
    $filename=(dir C:\Users\Test\AppData\Roaming\uTorrent | ?{!$_.PsIsContainer} | sort CreationTime -desc| select -first 1).Fullname
    $message.Body=$filename
    $smtp=new-object Net.Mail.SmtpClient('smtp.gmail.com',587)
    $smtp.EnableSSL = $true
    $smtp.Credentials=New-Object System.Net.NetworkCredential( 'me@gmail.com', 'MyGmailPW' );
    $smtp.Send($message)

    ReplyDelete
    Replies
    1. Hi Rick, you can definitely achieve this, and most of the heavy lifting should be done. What I would suggest is copy the script above change the part inside the Measure-Command with the email code. You should be able to just use the %N, if you want file/folder read the section above regarding how they work, it changes depending on the type of torrent.

      You will need to make some changes to the config reading, just remove what you don't need and set the rest to constants.

      To call the code from utorrent there is a setting to execute a command when and download finishes, Check out the start of the script for more info.
      The log_message can help to figure out what is happening when it is called.

      For the email password I would consider using a secure string in a config file. I have some code I will post to show how to use that.

      Let me know how you go.

      Delete
  2. Hello,

    I'm starting some automating of my own (I have a big background in VBscript, but want to use this "home project" as a first step in learning Powershell).

    I think there's a new variable in Utorrent (%K) that tells you if the torrent is multifile or single.
    Also can you share an example of the XML config file ? (I'm not sure yet how Powershell reads it so an example would be welcome)
    Thanks

    ReplyDelete
    Replies
    1. I'm actually not sure how the get-ScriptConfig (xml part) function works, I havent found anything about that on the web.

      Is it a function that you wrote and isnt included in this code ?

      Thanks

      Delete
    2. Thanks for letting me know about the new parameter %K. Will update the script to support it. get-ScriptConfig is a script I created. I use it so I can change params without needing to re sign the script. I will post the code however you can get work around it. What you need to do is set the three path variables that are set by $configFile.

      Delete
  3. Hi Chris,
    I really like your script you have written here.
    I use something similar to sort my torrents by TV show and Movies while copying them to different directories using VBS. I'm not quite happy with the way it's doing it right now as it's just calling a very generic script and not basing it on the torrent state changes so it ends up copying all completed files over and over again.

    I was wondering if there was any way I could use your code above but have the additional function of checking the torrent name and copying that particular torrent (File or Folder) to the correct directory (Movie or TV Show)?

    Would you be able to help me with this? I would much rather use Powershell than VBS.

    Thanks,
    William.

    ReplyDelete
    Replies
    1. Hi William, I would be happy to help you with your script. It would prob be easier to do over email. Let me know what you would like to achieve and where you are up to.

      Chris

      Delete