georgboe-ucli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. data/README.md +126 -0
  2. data/Rakefile +48 -0
  3. data/bin/ucli +142 -0
  4. data/lib/lsfile.rb +1 -0
  5. data/lib/torrent.rb +110 -0
  6. data/lib/utilities.rb +34 -0
  7. metadata +77 -0
@@ -0,0 +1,126 @@
1
+ ## ucli
2
+
3
+ Ucli is a simple commandline interface against uTorrent's WebUI.
4
+
5
+ ### Requirements
6
+ * curl
7
+ * trollop >= 1.10 (gem)
8
+ * json (gem)
9
+
10
+ ### Getting started
11
+
12
+ Ucli uses a yaml file to store your configuration. An examplefile will be generated at first run.
13
+
14
+ ~% ucli
15
+ No config file found.
16
+ Check out ~/.ucli_config.yml.example
17
+
18
+ Now edit this configuration file with your information and rename it to `.ucli_config.yml`.
19
+ You are now ready to use the commands.
20
+
21
+ ### Usage
22
+
23
+ #### List all torrents
24
+
25
+ Running the command `ucli --list` or `ucli -l` will give you a list of all the torrents in utorrent.
26
+ The output will look like this:
27
+
28
+ +-----------------------------------------------------------+----------+-------+
29
+ | Torrentname | Uploaded | Ratio |
30
+ +-----------------------------------------------------------+----------+-------+
31
+ | ubuntu-8.10-desktop-i386.iso | 1.24 GB| 1.82|
32
+ | *ubuntu-8.10-server-i386.iso* | 0.00 KB| 0.00|
33
+ +-----------------------------------------------------------+----------+-------+
34
+
35
+ The stars surrounding the last torrent means that the downloading has not completed yet.
36
+
37
+ #### Uploading torrents
38
+
39
+ You can upload torrents from your local machine to the webui the `ucli -u` command.
40
+ A label/category is required for all torrents.
41
+
42
+ ucli -u *.torrent -c "MISC"
43
+
44
+ The command above will upload all torrent files in your current working directory and
45
+ add the label "MISC" all of them.
46
+
47
+ #### Remove torrents
48
+
49
+ You can remove torrents with the `ucli -r` command.
50
+ You can specify a name or regex to match.
51
+ It will show you all the matches along with their shareratio and will not delete anything before you have confirmed it.
52
+
53
+ ~% ucli -r server
54
+ Found the following torrents
55
+ ============================
56
+ ubuntu-8.10-server-i386.iso (0.06)
57
+
58
+ Are you sure you want to remove these torrents? (y or n):
59
+
60
+ In this case there was only on matching torrent, but you can remove large amount of torrents using regexes.
61
+
62
+ #### Removing old tv episodes
63
+
64
+ `ucli --oldepisodes` looks for torrents of the same tv show. If you have two episode of the same show, it will suggest remove the oldest.
65
+ Let's say you have these episodes in uTorrent:
66
+
67
+ Tvshow.S01E01.HDTV-XviD-GROUP
68
+ Tvshow.S01E02.HDTV-XviD-GROUP
69
+
70
+ Another.show.S02E04.HDTV-XviD-GROUP
71
+ Another.show.S02E12.HDTV-XviD-GROUP
72
+
73
+ Now we try to run the command.
74
+
75
+ ~% ucli --oldepisodes
76
+ Found the following old episodes
77
+ ================================
78
+ Tvshow.S01E01.HDTV-XviD-GROUP (1.49)
79
+ Another.show.S02E04.HDTV-XviD-GROUP (1.20)
80
+
81
+ Do you want to remove them? (y or n):
82
+
83
+ #### Listing files in torrents
84
+
85
+ `ucli --ls` command will show you all the files with filesizes in the torrents matching you input.
86
+
87
+ ~% ucli --ls server
88
+ ubuntu-8.10-server-i386.iso
89
+ ===========================
90
+ ubuntu-8.10-server-i386.iso (637.32 MB)
91
+
92
+
93
+ ### Notes
94
+
95
+ * I have not tried this script on Windows, and I don't think uploading torrents will work. This is due to the difference in the way windows passes arguments to programs as opposed to unix/linux. The other functionality shouldn't be a problem though.
96
+
97
+ * This application was made to work with uTorrent WebUI 0.361. It will not work with earlier version, but probably will work with later versions.
98
+
99
+ * You can actually use a regex for all the options that requires input to match a torrent. `ucli -r "\d\d"` will remove all torrents containing two digits.
100
+
101
+ [More information about the Web UI API](http://forum.utorrent.com/viewtopic.php?id=25661#p258858 "Web UI API on the uTorrent forum")
102
+
103
+ ### LICENSE:
104
+
105
+ (The MIT License)
106
+
107
+ Copyright (c) 2009 Georg Alexander Bøe
108
+
109
+ Permission is hereby granted, free of charge, to any person obtaining
110
+ a copy of this software and associated documentation files (the
111
+ 'Software'), to deal in the Software without restriction, including
112
+ without limitation the rights to use, copy, modify, merge, publish,
113
+ distribute, sublicense, and/or sell copies of the Software, and to
114
+ permit persons to whom the Software is furnished to do so, subject to
115
+ the following conditions:
116
+
117
+ The above copyright notice and this permission notice shall be
118
+ included in all copies or substantial portions of the Software.
119
+
120
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
121
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
122
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
123
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
124
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
125
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
126
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'rubygems/specification'
4
+ require 'rake'
5
+ require 'rake/gempackagetask'
6
+
7
+ GEM = "ucli"
8
+ GEM_VERSION = "0.2.0"
9
+ SUMMARY = "Commandline interface to uTorrent WebUI 0.361"
10
+ AUTHOR = "Georg Alexander Bøe"
11
+ EMAIL = "georg.boe@gmail.com"
12
+
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.name = GEM
16
+ s.version = GEM_VERSION
17
+ s.platform = Gem::Platform::RUBY
18
+ s.summary = SUMMARY
19
+ s.require_paths = ['lib']
20
+ s.files = ['README.md', 'Rakefile'] + Dir["{lib,bin,spec}/**/*"]
21
+ s.bindir = 'bin'
22
+ s.executables = ['ucli']
23
+
24
+ s.author = AUTHOR
25
+ s.email = EMAIL
26
+
27
+ s.add_dependency 'json'
28
+ s.add_dependency 'trollop', '>=1.10'
29
+
30
+ s.rubyforge_project = GEM # GitHub bug, gem isn't being build when this miss
31
+ end
32
+
33
+
34
+ Rake::GemPackageTask.new(spec) do |pkg|
35
+ pkg.gem_spec = spec
36
+ end
37
+
38
+ desc "Install the gem locally"
39
+ task :install => [:package] do
40
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
41
+ end
42
+
43
+ desc "Create a gemspec file"
44
+ task :make_spec do
45
+ File.open("#{GEM}.gemspec", "w") do |file|
46
+ file.puts spec.to_ruby
47
+ end
48
+ end
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'open-uri'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'trollop'
7
+ require 'ostruct'
8
+
9
+ require 'lsfile.rb'
10
+ require 'torrent.rb'
11
+ require 'utilities.rb'
12
+
13
+ PROGRAM_VERSION = '0.2.0'
14
+
15
+ begin
16
+
17
+ file = YAML.load_file(File.expand_path('~/.ucli_config.yml'))
18
+ @@settings = OpenStruct.new(file)
19
+ rescue Exception => e
20
+ STDERR.puts "No config file found.\nCheck out ~/.ucli_config.yml.example"
21
+ open(File.expand_path('~/.ucli_config.yml.example'), 'w+') do |file|
22
+ file.puts "
23
+ # rename this file to '.ucli_config.yml'
24
+ host: ipadrress_or_hostname
25
+ port: 8080
26
+ username: your_username
27
+ password: your_password
28
+ ".strip
29
+ end
30
+ exit 1
31
+ end
32
+
33
+ opts = Trollop::options do
34
+ version "utorrent version #{PROGRAM_VERSION}"
35
+ opt :list, "List all torrents"
36
+ opt :upload, "Uploads the given torrentfiles", :type => :ios
37
+ opt :category, "Category for uploaded file", :type => :string, :default => nil
38
+ opt :oldepisodes, "Show and remove old tv episodes"
39
+ opt :remove, "Remove torrents by name", :type => :strings
40
+ opt :ls, "Show files for a given torrent", :type => :string
41
+ end
42
+
43
+ Trollop.die("No option selected") unless opts[:list] or opts[:upload] or opts[:download] or opts[:oldepisodes] or opts[:remove] or opts[:ls]
44
+ Trollop.die("Category is required for uploaded torrent") if opts[:upload] and !opts[:category]
45
+
46
+ if opts[:list]
47
+ # Textmate & DTerm tput hack
48
+ if `echo $TERM` == "dumb\n"
49
+ TORRENTNAME_WIDTH = 58
50
+ else
51
+ TORRENTNAME_WIDTH = (`tput cols`.to_i != 0 ? (`tput cols`.to_i) - 22 : 58)
52
+ end
53
+
54
+ Utilities.print_separator(TORRENTNAME_WIDTH)
55
+ puts "| #{'Torrentname'.ljust(TORRENTNAME_WIDTH)}|#{'Uploaded'.center(10)}|#{'Ratio'.center(7)}|"
56
+ Utilities.print_separator(TORRENTNAME_WIDTH)
57
+ Torrent.all.sort { |x,y| x.name <=> y.name }.each { |x| x.name = "*#{x.name}*" unless x.done == 100.0 }.each do |torrent|
58
+ puts "| #{Utilities.cut_string(torrent.name, TORRENTNAME_WIDTH).ljust(TORRENTNAME_WIDTH)}|#{torrent.uploaded.to_s.rjust(10)}|#{torrent.ratio.to_s.rjust(7)}|"
59
+ end
60
+ Utilities.print_separator(TORRENTNAME_WIDTH)
61
+
62
+ elsif opts[:upload]
63
+
64
+ opts[:upload].each do |torrent|
65
+
66
+ path = File.expand_path(torrent.path)
67
+ result = Torrent.upload_torrent(path, opts[:category])
68
+
69
+ if result
70
+ puts "#{File.basename(path)} successfully uploaded."
71
+ else
72
+ puts "Uploading torrent failed."
73
+ end
74
+ end
75
+
76
+ delete = Utilities.agree?("Do you want to delete the .torrent files?")
77
+ if delete
78
+ opts[:upload].each do |e|
79
+ path = File.expand_path(e.path)
80
+ File.delete(path)
81
+ puts "#{File.basename(path)} deleted."
82
+ end
83
+ end
84
+
85
+ elsif opts[:oldepisodes]
86
+ old_episodes = Torrent.find_old_episodes
87
+
88
+ if (old_episodes.length == 0)
89
+ puts "No old episodes found."
90
+ exit 0
91
+ end
92
+
93
+ puts "Found the following old episodes"
94
+ puts "================================"
95
+ old_episodes.each { |episode| puts "#{episode.name} (#{episode.ratio})" }
96
+ puts
97
+ should_remove = Utilities.agree?("Do you want to remove them?")
98
+
99
+ if should_remove
100
+ old_episodes.each do |e|
101
+ e.remove
102
+ end
103
+ puts "Old episodes removed."
104
+ end
105
+
106
+ elsif opts[:remove]
107
+ torrents = opts[:remove].inject([]) { |sum, x| sum += Torrent.find_by_name(x) }
108
+
109
+ if torrents.length == 0
110
+ puts "No torrents mached #{opts[:remove]}"
111
+ exit 1
112
+ end
113
+
114
+ puts "Found the following torrents"
115
+ puts "============================"
116
+ torrents.each { |torrent| puts "#{torrent.name} (#{torrent.ratio})" }
117
+ puts
118
+ should_remove = Utilities.agree?("Are you sure you want to remove these torrents?")
119
+
120
+ if should_remove
121
+ torrents.each do |torrent|
122
+ torrent.remove
123
+ puts "#{torrent.name} removed."
124
+ end
125
+ end
126
+
127
+ elsif opts[:ls]
128
+ torrents = Torrent.find_by_name(opts[:ls])
129
+
130
+ if torrents.length == 0
131
+ puts "No torrents mached #{opts[:ls]}"
132
+ exit 1
133
+ end
134
+
135
+ torrents.each_with_index do |torrent, index|
136
+ puts if index != 0
137
+ puts torrent.name
138
+ puts "=" * torrent.name.length
139
+ torrent.list_files.each { |file| puts "#{file.name} (#{file.size})" }
140
+ end
141
+
142
+ end
@@ -0,0 +1 @@
1
+ class LsFile < Struct.new(:name, :size); end
@@ -0,0 +1,110 @@
1
+ class Torrent
2
+ require 'lsfile.rb'
3
+
4
+ attr_accessor :hash, :name, :uploaded, :ratio, :label, :done, :basename
5
+ def initialize(hash, name, uploaded, ratio, label, done)
6
+ @hash = hash
7
+ @name = name
8
+ @uploaded = uploaded
9
+ @ratio = ratio
10
+ @done = done
11
+ @label = label
12
+ end
13
+
14
+ def set_category(category)
15
+ open("http://#{@@settings.host}:#{@@settings.port}/gui/?action=setprops&s=label&hash=#{@hash}&v=#{category}", :http_basic_authentication => [@@settings.username, @@settings.password])
16
+ end
17
+
18
+ def remove
19
+ template = "http://#{@@settings.host}:#{@@settings.port}/gui/?action=remove&hash=%s"
20
+ open(template % @hash, :http_basic_authentication => [@@settings.username, @@settings.password]) {|f| f.read }
21
+ end
22
+
23
+ def self.all
24
+ json = JSON.parse(open("http://#{@@settings.host}:#{@@settings.port}/gui/?list=1", :http_basic_authentication => [@@settings.username, @@settings.password]).read)
25
+ torrents = Array.new
26
+
27
+ json['torrents'].each do |item|
28
+ torrents << Torrent.new(item[0], item[2], format_size(item[6].to_i), format_ratio(item[7]), item[11], (item[4].to_f / 10.00))
29
+ end
30
+ torrents
31
+ end
32
+
33
+ def self.upload_torrent(file, category=nil)
34
+ result = system("curl --basic --user #{@@settings.username}:#{@@settings.password} -F \"torrent_file=@#{file};type=application/x-bittorrent\" 'http://#{@@settings.host}:#{@@settings.port}/gui/?action=add-file' &> /dev/null")
35
+
36
+ if result and category
37
+ Torrent.all.last.set_category(category)
38
+ end
39
+ end
40
+
41
+ def self.find_old_episodes
42
+
43
+ episodes = all.reject {|x| !x.name.match(/(S\d+(\.|)E\d+)|(\d+x\d+)/)}
44
+ old_episodes = Array.new
45
+
46
+ episodes.each { |e| e.basename = e.name.gsub(/(.+)(\.S\d+(\.|)E\d+|\d+x\d+).+/, '\1')}.sort! {|x,y| x.name <=> y.name }
47
+
48
+ duplicates = Hash.new
49
+
50
+ episodes.each { |e|
51
+ if duplicates.has_key?(e.basename)
52
+ duplicates[e.basename] += 1
53
+ else
54
+ duplicates[e.basename] = 1
55
+ end
56
+ }
57
+
58
+ duplicates.reject { |k,v| v == 1 }
59
+
60
+ episodes.each_with_index do |episode, i|
61
+
62
+ if !duplicates.has_key?(episode.basename)
63
+ next
64
+ end
65
+
66
+ next if i == episodes.length-1
67
+
68
+ old_episodes << episode unless episodes[i+1].basename != episode.basename
69
+ end
70
+
71
+ old_episodes
72
+ end
73
+
74
+ def self.find_by_name(name)
75
+ torrents = Array.new
76
+ all.each { |torrent| torrents << torrent if torrent.name =~ Regexp.new(name, true)}
77
+ torrents
78
+ end
79
+
80
+ def list_files
81
+ json = JSON.parse(open("http://#{@@settings.host}:#{@@settings.port}/gui/?action=getfiles&hash=#{@hash}", :http_basic_authentication => [@@settings.username, @@settings.password]).read)
82
+ files = Array.new
83
+ json['files'][1].each { |file| files << LsFile.new(file[0], Torrent.format_size(file[1]))}
84
+ files
85
+ end
86
+
87
+ private
88
+
89
+ def self.format_size(bytes)
90
+
91
+ bytes = bytes.to_f
92
+
93
+ if bytes > 1024**3
94
+ return "%1.2f GB" % (bytes / 1024**3)
95
+ elsif bytes > 1024**2
96
+ return "%1.2f MB" % (bytes / 1024**2)
97
+ else
98
+ return "%1.2f KB" % (bytes / 1024)
99
+
100
+ end
101
+
102
+ end
103
+
104
+ def self.format_ratio(ratio)
105
+ return "0.00" if ratio.to_s.length == 1
106
+
107
+ "%0.2f" % (ratio.to_f / 1000.00)
108
+ end
109
+
110
+ end
@@ -0,0 +1,34 @@
1
+ class Utilities
2
+
3
+ def self.cut_string(string, length=58)
4
+ if string.length > length-1
5
+ string = string[0,length-4] + '...'
6
+ end
7
+ string
8
+ end
9
+
10
+ def self.print_separator(length=58)
11
+ puts "+" + ("-" * length) + "-+----------+-------+"
12
+ end
13
+
14
+ def self.agree?(message)
15
+ valid = false
16
+ answer = false
17
+
18
+ while (!valid) do
19
+
20
+ print "#{message} (y or n): "
21
+ line = gets.strip
22
+
23
+ if (line.downcase == 'y')
24
+ valid = true
25
+ answer = true
26
+ elsif (line.downcase == 'n')
27
+ valid = true
28
+ answer = false
29
+ end
30
+ end
31
+ answer
32
+ end
33
+
34
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: georgboe-ucli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - "Georg Alexander B\xC3\xB8e"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-14 00:00:00 -07:00
13
+ default_executable: ucli
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: trollop
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "1.10"
34
+ version:
35
+ description:
36
+ email: georg.boe@gmail.com
37
+ executables:
38
+ - ucli
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - README.md
45
+ - Rakefile
46
+ - lib/lsfile.rb
47
+ - lib/torrent.rb
48
+ - lib/utilities.rb
49
+ - bin/ucli
50
+ has_rdoc: false
51
+ homepage:
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project: ucli
72
+ rubygems_version: 1.2.0
73
+ signing_key:
74
+ specification_version: 2
75
+ summary: Commandline interface to uTorrent WebUI 0.361
76
+ test_files: []
77
+