Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Convert added torrents data from uTorrent (resume.dat) to qBittorrent (*.fastresume) format
# encoding: utf-8
# USE AT OWN RISK
#
# Follow this guide to install Ruby on Windows (step 1 and 2): https://forwardhq.com/support/installing-ruby-windows
# Install gems (with admin cmd prompt):
# https://rubygems.org/gems/bencode
# https://rubygems.org/gems/rest_client
#
# Edit datpath below to point to your utorrent resume.dat file
#
# Run this file with cmd prompt after installing Ruby
# Start qBittorrent and let all torrents be checked
# Enjoy!
#
Encoding.default_internal = Encoding::UTF_8
Encoding.default_external = Encoding::UTF_8
require 'rubygems'
require 'bencode'
require 'rest_client'
require 'pp'
require 'win32ole'
require 'fileutils'
require 'json'
require 'pathname'
class Array
def rinclude? (reg)
return self.include?(reg) unless reg.is_a?(Regexp)
self.each do |i|
return true if i.is_a?(String) and i =~ reg
end
return false
end
end
def isUtRunning?
wmi = WIN32OLE.connect("winmgmts://")
procs = wmi.ExecQuery("SELECT * FROM win32_process WHERE Name = 'utorrent.exe'")
return true if procs.Count > 0
return false
end
def yayNay(userPrompt, defaultIsYes=true)
resp = ''
rv = defaultIsYes
begin
puts "\n"
print userPrompt
if defaultIsYes then
print ' [Y/n] '
else
print ' [y/N] '
end
resp = gets
end until resp =~ /^($|[yn]|yes|no)$/i
rv = true if resp =~ /^y/i
rv = false if resp =~ /^n/i
return rv
end
def prompt(userPrompt, default=nil)
resp = ''
rv = default
begin
puts "\n"
print userPrompt
unless default.nil? or default.empty? then
print " [#{default}] "
else
print ' '
end
resp = gets
break unless resp.chomp.empty?
break unless default.nil? or default.empty?
end while true
rv = resp unless resp.chomp.empty?
return rv.chomp
end
def backupResumeDat(datpath)
newname = ''
begin
newname = "#{datpath}.#{Time.now.strftime("%Y-%m-%d.%H-%M-%S.bak")}"
end while File.exists?(newname)
FileUtils.cp(datpath, newname)
end
def tPunchup(res, tor, dotracker = true, doname = true)
rv = tor.dup
if dotracker then
utrax = res['trackers']
# puts utrax
if utrax.size > 1 then
unless rv['announce-list'].nil?
unless rv['announce-list'].flatten.uniq.sort == utrax.uniq.sort then
rv['announce-list'] = utrax.uniq.map {|x| [x]}
end
else
unless rv['announce'] == utrax.first then
rv['announce'] = utrax.first
end
end
end
end
if doname then
path = Pathname.new(res['path'])
single = !(rv['info'].has_key?('files'))
unless single
tname = rv['info']['name']
rv['info']['name'] = path.basename.to_s
end
end
rv
end
def mkfr(res, tor)
frestmpl = {
'active_time' => 0 , # runtime
'added_time' => 0 , # added_on
'allocation' => 'full' ,
'announce_to_dht' => 1 ,
'announce_to_lsd' => 1 ,
'announce_to_trackers' => 1 ,
'auto_managed' => 1 ,
'banned_peers' => '' ,
'banned_peers6' => '' ,
'blocks per piece' => 0 , # ("piece length" from .torrent) / ("block size" resume.dat) - range [1,256]
'completed_time' => 0 , # completed_on
'download_rate_limit' => 0 ,
'file sizes' => [
[
0 , # File 1, size in bytes
0 # File 1, modified date (timestamp) or (modtimes array in resume)
] ,
[
0 , # File 2, size in bytes
0 # File 2, mtime (ts)
] ,
[
0 ,
0
]
] ,
'file-format' => 'libtorrent resume file' , # req
'file-version' => 1 , # req
'file_priority' => [ # prio bitfield
2 , # File 1, High
0 , # File 2, Do not download
1 # File 3, Normal
] ,
'finished_time' => 0 ,
'info-hash' => '', # Digest::SHA1.digest('infohashbinarydata') , # tdb['info'].bencode
'last_download' => 0 ,
'last_scrape' => 0 ,
'last_seen_complete' => 0 ,
'last_upload' => 0 ,
'libtorrent-version' => '1.0.6.0' ,
'mapped_files' => [
'relative\path\to\file1.ext' , # File 1
'r\p\t\file2.ext' , # File 2
'file3.ext' # File 3
] ,
'max_connections' => 100 ,
'max_uploads' => 16777215 ,
'num_downloaders' => 16777215 ,
'num_incomplete' => 0 ,
'num_seeds' => 0 ,
'paused' => 0, # 'started' - 0 = stopped, 1 = force , 2 = start
'peers' => '' ,
'peers6' => '' ,
'piece_priority' => '' , # "\x01"*1399 , # * num pieces?
'pieces' => '', #"\x01"*1399 , # * num pieces?
'seed_mode' => 0 ,
'seeding_time' => 0 ,
'sequential_download' => 0 ,
'super_seeding' => 0 ,
'total_downloaded' => 0 , # downloaded field
'total_uploaded' => 0 , # uploaded field
'upload_rate_limit' => 0 , #upspeed
'trackers' => [
[
'https://tracker' # direct match to trackers
]
]
}
fr = frestmpl.dup
npieces = tor['info']['pieces'].size / 20 # SHA1 hash is 20 bytes
fr['added_time'] = res['added_on'].to_i
fr['completed_time'] = res['completed_on'].to_i
fr['active_time'] = res['runtime'].to_i
fr['seeding_time'] = fr['active_time']
fr['blocks per piece'] = tor['info']['piece length'].to_i / res['blocksize'].to_i
fr['info-hash'] = Digest::SHA1.digest(tor['info'].bencode)
fr['paused'] = 1 # Always add torrents in paused state
# fr['paused'] = 1 if res['started'] == 0
# puts res['started']
# puts " paused : "+fr['paused'].to_s
fr['auto_managed'] = 0
fr['total_downloaded'] = res['downloaded'].to_i
fr['total_uploaded'] = res['uploaded'].to_i
fr['upload_rate_limit'] = res['upspeed'].to_i
fr['trackers'] = res['trackers'].map {|tracker| [tracker] }
fr['piece_priority'] = res['have'].unpack('b*').first.scan(/./).map {|i| (i.to_i(2)*1) }.pack('C'*npieces) #= "\x01" * npieces
#~ fr['pieces'] = res['have'].unpack('b*').first.scan(/./).pack('b'*npieces)
fr['pieces'] = res['have'].unpack('b*').first.scan(/./).map {|i| (i.to_i(2)*1) }.pack('C'*npieces)
fr['finished_time'] = (Time.now - Time.at(fr['completed_time'])).to_i
unless fr['finished_time'].to_i == 0 then
fr['last_seen_complete'] = Time.now.to_i
end
#~ fr['finished_time'] = fr['completed_time']
fr['last_download'] = fr['finished_time']
fr['last_scrape'] = fr['finished_time']
fr['last_upload'] = fr['finished_time']
# Per file fields:
##########
# mapped_files
# file_priority
# file sizes
fr['file_priority'] = res['prio'].unpack('H*').first.scan(/../).map do |h|
next 1 if (1..8) === h.to_i(16)
next 2 if (9..15) === h.to_i(16)
next 0
end
fr['mapped_files'] = []
fr['file sizes'] = []
fmt = 0
unless tor['info']['files'].nil? then
tor['info']['files'].each_index do |findex|
tfile = tor['info']['files'][findex]
fr['mapped_files'] << Pathname.new('').join(*(tfile['path'])).to_s.force_encoding('UTF-8').gsub('/', '\\')
unless res['modtimes'].nil? then
fmt = res['modtimes'][findex].to_i # file time to avoid checking / not presen in ut2.2
else
fmt = 0
end
thisFile = Pathname.new(res['path'].to_s.force_encoding('UTF-8').gsub('/', '\\')).join(fr['mapped_files'].last)
if thisFile.exist? then
fmt = thisFile.mtime.to_i unless fmt > 0
# puts " ------debug ---------"
# puts thisFile.to_s
# puts " File ok"
#~ puts thisFile.mtime.to_i
else # if a file is missing then set state to stopped
fr['paused'] = 1
# puts " ------debug ---------"
# puts tfile.to_s
# puts thisFile.to_s
# puts " File missing, paused"
end
# puts " - file: " + thisFile.to_s + fmt.to_s
if fr['file_priority'][findex] !=0 then
fr['file sizes'] << [ tfile['length'].to_i, fmt ]
else
# puts thisFile.to_s
fr['file sizes'] << [ 0, 0 ]
end
end
fr['mapped_files'] = []
savepath = Pathname.new(res['path']).dirname.to_s.force_encoding('UTF-8')
savepath << '\\' unless savepath.end_with?('\\')
fr['save_path'] = savepath.to_s.force_encoding('ASCII-8BIT') #res['path']
savepath2 = Pathname.new(res['path']).dirname.to_s.force_encoding('UTF-8').gsub('\\', '/')
savepath2 << '/' unless savepath2.end_with?('/')
fr['qBt-savePath'] = savepath2.to_s.force_encoding('ASCII-8BIT') #res['path']
# puts "---- debug start -----"
# puts " Multi file torrent"
# puts " qBt-savePath: " + savepath2.to_s
# puts " save_path: " + savepath.to_s
# puts "----- debug end ------"
else
savepath = Pathname.new(res['path']).dirname.to_s.force_encoding('UTF-8').gsub('\\', '/')
savepath << '/' unless savepath.end_with?('/')
fr['qBt-savePath'] = savepath.to_s.force_encoding('ASCII-8BIT')
# puts "---- debug start -----"
# puts " Single file torrent"
# puts " qBt-savePath: " + savepath.to_s
# puts " full path: " + res['path'].to_s
# puts " single path: " + Pathname.new(res['path']).dirname.to_s
# puts " file name: " + tor['info']['name'].to_s
# puts " size: " + tor['info']['length'].to_s
# puts "----- debug end ------"
# fr['mapped_files'] << tor['info']['name'].to_s.gsub('\\', '/')
unless res['modtimes'].nil? then
fmt = res['modtimes'][0].to_i # file time to avoid checking / not presen in ut2.2
else
fmt = 0
end
thisFile = Pathname.new(res['path'])
if thisFile.exist? then
fmt = thisFile.mtime.to_i unless fmt > 0
# puts " filetime: " + thisFile.mtime.to_s
end
if fr['file_priority'][0] !=0 then
fr['file sizes'] << [ tor['info']['length'].to_i , fmt ]
else
# puts thisFile.to_s
fr['file sizes'] << [ 0, 0 ]
end
# puts tor['info']['name'].to_s
# puts tor['info'].to_s
# puts "---------"
end
##########
# qBittorrent 3.1+ Fields
##########
# fr['qBt-savePath'] = res['path'] # handled above
#~ fr['qBt-ratioLimit'] = '' # Blank because qBt actions on limit aren't as diverse
fr['qBt-label'] = res['label'].to_s.force_encoding('ASCII-8BIT') if res.has_key?('label')
fr['qBt-queuePosition'] = -1 # -1 for completed
fr['qBt-seedDate'] = fr['completed_time']
fr['qBt-ratioLimit'] = '-2' # -2 = Use Global, -1 = No limit, other number = actual ratio?
#~ fr['qBt-ratioLimit'] = (res['wanted_ratio'].to_f / 1000).to_s
# ^ uTorrent has this setting but the actions you can take are different,
# so you might not want to bring this over.
fr
end
#########################
#########################
if not defined?(Ocra)
# Check required conditions
if ARGV.count != 2
puts "\n"
puts "Usage ut2qt.exe \"C:\\Users\\Administrator\\AppData\\Roaming\\uTorrent\\resume.dat\" \"C:\\Users\\Administrator\\AppData\\Local\\qBittorrent\\BT_backup\\\""
puts "Don't forget to delete qBittorrent-resume.ini from "+ ENV['APPDATA']+"\\qBittorrent\ afterwards!"
puts "\n"
exit(-1)
end
end
if not defined?(Ocra) then
datpath=ARGV[0]
outpath=ARGV[1].chomp("\\").chomp("\"")
else
# datpath = "G:\\uTorrent\\resume.dat" # EDIT THIS!
# outpath = 'D:\Temp\BT_backup'
datpath = "C:\\Users\\Administrator\\AppData\\Roaming\\uTorrent\\resume.dat" # EDIT THIS!
outpath = 'C:\Users\ADMINI~1\AppData\Local\QBITTO~1\BT_BAC~1'
end;
########################
########################
if isUtRunning? then
puts "uTorrent is still running! Close it and wait for the process to disappear before running this again."
exit -1
end
#datpath = "C:\\Users\\Administrator\\AppData\\Roaming\\uTorrent\\resume.dat" # EDIT THIS!
#print "Backing up dat file..."
#backupResumeDat(datpath)
#puts "complete."
print "Parsing dat file..."
q = BEncode.load_file(datpath)
puts "complete."
puts "Consistency check..."
torrents = {}
q.each do |k,v|
#~ raise "Not a hash" unless v.is_a?(Hash)
next unless v.is_a?(Hash)
raise "No caption" unless v.has_key?('caption')
raise "#{v['caption']} :- No added on" unless v.has_key?('added_on')
raise "#{v['caption']} :- No completed on" unless v.has_key?('completed_on')
torrents[k] = v
end
puts "...Done\n"
#~ torrents.sort_by! {|v| v['completed_on'].to_i }
c = 0
d = 0
#outpath = 'C:\Temp\BT_backup'
raise "'#{outpath}' does not exist." unless File.exist?(outpath)
puts "Found this many torrents: "
puts torrents.length
total = torrents.length
puts "\nProcessing, please wait..."
torrents.each do |k, v|
d += 1
tf = Pathname.new(datpath).dirname + k
begin
q = BEncode.load_file(tf.to_s.force_encoding('UTF-8'))
rescue => error
puts "load_file error, skipping: "
puts k.to_s
# puts tf.to_s.encoding.to_s
puts "error: " + error.message
puts "backtrace: " + error.backtrace.to_s
next
end
puts d.to_s+"/"+total.to_s+ " " + k
outp = Pathname.new(outpath)
tfile = tPunchup(v, q, false, false)
hash = Digest::SHA1.hexdigest(tfile['info'].bencode).downcase
if outp.join("#{hash}.torrent").exist? || outp.join("#{hash}.fastresume").exist?
#puts "Torrent processed already, skipping: " + k
next
end
# ^ Skip this if it has been done so that subsequent runs don't re-process
begin
frfile = mkfr(v, tfile)
rescue => error
puts "mkfr error, skipping: " + k
puts "error: " + error.message
puts "backtrace: " + error.backtrace.to_s
next
end
begin
f = File.new(outp.join("#{hash}.torrent").to_s.force_encoding('UTF-8'), 'wb')
f.write(tfile.bencode)
f.close
f = File.new(outp.join("#{hash}.fastresume").to_s.force_encoding('UTF-8'), 'wb')
f.write(frfile.bencode)
f.close
c += 1
rescue => error
puts "output error, skipping: " + k
puts "error: " + error.message
puts "backtrace: " + error.backtrace.to_s
puts frfile.to_s
next
end
end
puts "Done! Exported this many torrents: "
puts c
@fish3010

This comment has been minimized.

Copy link

@fish3010 fish3010 commented Sep 17, 2018

Torrents with multiple tracker links fail to convert. Some don't convert at all and others convert as trackerless torrents.

@Kaos-Industries

This comment has been minimized.

Copy link

@Kaos-Industries Kaos-Industries commented Sep 25, 2019

Whenever I run the script as instructed, I just get output telling me usage should be ut2qt.exe. Whenever I run ut2qt.exe I get command not found, and there's definitely no such binary on my system. What gives here?

@Kaos-Industries

This comment has been minimized.

Copy link

@Kaos-Industries Kaos-Industries commented Sep 25, 2019

Whenever I run the script as instructed, I just get output telling me usage should be ut2qt.exe. Whenever I run ut2qt.exe I get command not found, and there's definitely no such binary on my system. What gives here?

Scratch that, I managed to get this working, but the output there is very confusing. Maybe add something to the usage instructions to say that you can run either the binary or the script, with the two needed paths as arguments.

Also, looking at the actual imported torrents after restarting Qbittorrent, it seems that the vast majority are missing, presumably because I've renamed and/or moved the downloaded files to somewhere else like I usually do. Unfortunately until the script can be adapted to work with those torrents, it makes it useless for me personally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment