nyaa 0.3.0 → 1.0.2
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.
- data/.gitignore +3 -0
- data/Gemfile +2 -1
- data/README.md +31 -11
- data/Rakefile +7 -4
- data/bin/nyaa +70 -41
- data/lib/nyaa.rb +26 -3
- data/lib/nyaa/browser.rb +122 -163
- data/lib/nyaa/cli.rb +75 -0
- data/lib/nyaa/constants.rb +124 -0
- data/lib/nyaa/downloader.rb +65 -0
- data/lib/nyaa/search.rb +113 -0
- data/lib/nyaa/torrent.rb +65 -0
- data/lib/nyaa/ui.rb +260 -0
- data/lib/nyaa/version.rb +2 -1
- data/nyaa.gemspec +1 -3
- data/screenshots/v1.0.0_browse.png +0 -0
- data/screenshots/v1.0.0_help.png +0 -0
- data/screenshots/v1.0.0_search.png +0 -0
- metadata +14 -32
- data/.gitattributes +0 -1
- data/.travis.yml +0 -5
- data/AUTHORS +0 -4
- data/Gemfile.lock +0 -24
- data/HACKING +0 -13
- data/test/lib/nyaa/browser_test.rb +0 -4
- data/test/lib/nyaa/version_test.rb +0 -7
- data/test/test_helper.rb +0 -2
data/lib/nyaa/cli.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Nyaa
|
3
|
+
class CLI
|
4
|
+
def self.parse(args)
|
5
|
+
@config = {}
|
6
|
+
|
7
|
+
opts = OptionParser.new do |opt|
|
8
|
+
opt.banner = 'Usage: nyaa [options] QUERY'
|
9
|
+
opt.separator ''
|
10
|
+
opt.separator 'Specific options:'
|
11
|
+
|
12
|
+
@config[:category] = 'anime_english'
|
13
|
+
opt.on('-c', '--category=CATEGORY', 'Select a search category (default: anime_english)') do |cat|
|
14
|
+
@config[:category] = cat
|
15
|
+
end
|
16
|
+
|
17
|
+
@config[:filter] = 'show_all'
|
18
|
+
opt.on('-f', '--filter=FILTER', 'Filter for search query (default: show_all)') do |filter|
|
19
|
+
@config[:filter] = filter
|
20
|
+
end
|
21
|
+
|
22
|
+
@config[:output] = Dir.pwd
|
23
|
+
opt.on('-o', '--output-path=PATH', 'Output directory for downloads') do |output|
|
24
|
+
@config[:output] = output
|
25
|
+
end
|
26
|
+
|
27
|
+
opt.on('-b', '--batch', 'Batch mode for scripting (first page only)') do |batch|
|
28
|
+
@config[:batch] = batch
|
29
|
+
end
|
30
|
+
|
31
|
+
opt.on('-t', '--classic', 'Use the old interface (deprecated)') do |classic|
|
32
|
+
@config[:classic] = classic
|
33
|
+
end
|
34
|
+
|
35
|
+
opt.on('-v', '--version', 'Print version info') do
|
36
|
+
puts "nyaa #{Nyaa::VERSION}"
|
37
|
+
puts "Copyright (c) 2013 David Palma"
|
38
|
+
puts "https://github.com/mistofvongola/nyaa"
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
opt.on_tail '-h', '--help', 'Show usage info' do
|
43
|
+
puts opts
|
44
|
+
puts %s{
|
45
|
+
Categories:
|
46
|
+
anime_all, anime_raw, anime_english, anime_nonenglish, anime_music_video
|
47
|
+
books_all, books_raw, books_english, books_nonenglish
|
48
|
+
live_all, live_raw, live_english, live_nonenglish, live_promo
|
49
|
+
audio_all, audio_lossless, audio_lossy
|
50
|
+
pictures_all, pictures_photos, pictures_graphics,
|
51
|
+
software_all, software_apps, software_games
|
52
|
+
|
53
|
+
Filters:
|
54
|
+
show_all, filter_remakes, trusted_only, aplus_only}
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# grab leftovers
|
60
|
+
@config[:query] = opts.parse!(args).join(' ')
|
61
|
+
|
62
|
+
unless Nyaa::CATS.has_key?(@config[:category].to_sym)
|
63
|
+
puts "#{@config[:category]} is not a valid category"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
unless Nyaa::FILS.has_key?(@config[:filter].to_sym)
|
68
|
+
puts "#{@config[:filter]} is not a valid filter"
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
|
72
|
+
@config
|
73
|
+
end # parse
|
74
|
+
end # CLI
|
75
|
+
end # Nyaa
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Nyaa
|
3
|
+
|
4
|
+
BASE_URL = 'http://www.nyaa.eu/?page=search'
|
5
|
+
PSIZE = 100
|
6
|
+
|
7
|
+
CATS = {
|
8
|
+
:all_categories => {
|
9
|
+
:id => '0_0',
|
10
|
+
:title => 'All Categories'
|
11
|
+
},
|
12
|
+
:anime_all => {
|
13
|
+
:id => '1_0',
|
14
|
+
:title => 'Anime'
|
15
|
+
},
|
16
|
+
:anime_raw => {
|
17
|
+
:id => '1_11',
|
18
|
+
:title => 'Anime >> Raw Anime'
|
19
|
+
},
|
20
|
+
:anime_english => {
|
21
|
+
:id => '1_37',
|
22
|
+
:title => 'Anime >> English-translated Anime'
|
23
|
+
},
|
24
|
+
:anime_nonenglish => {
|
25
|
+
:id => '1_38',
|
26
|
+
:title => 'Anime >> Non-English-translated Anime'
|
27
|
+
},
|
28
|
+
:anime_music_video => {
|
29
|
+
:id => '1_32',
|
30
|
+
:title => 'Anime >> Anime Music Video'
|
31
|
+
},
|
32
|
+
:books_all => {
|
33
|
+
:id => '2_0',
|
34
|
+
:title => 'Books'
|
35
|
+
},
|
36
|
+
:books_raw => {
|
37
|
+
:id => '2_13',
|
38
|
+
:title => 'Books >> Raw Books'
|
39
|
+
},
|
40
|
+
:books_english => {
|
41
|
+
:id => '2_12',
|
42
|
+
:title => 'Books >> English-scanlated Books'
|
43
|
+
},
|
44
|
+
:books_nonenglish => {
|
45
|
+
:id => '2_39',
|
46
|
+
:title => 'Books >> Non-English-scanlated Books'
|
47
|
+
},
|
48
|
+
:audio_all => {
|
49
|
+
:id => '3_0',
|
50
|
+
:title => 'Audio'
|
51
|
+
},
|
52
|
+
:audio_lossless => {
|
53
|
+
:id => '3_14',
|
54
|
+
:title => 'Audio >> Lossless Audio'
|
55
|
+
},
|
56
|
+
:audio_lossy => {
|
57
|
+
:id => '3_15',
|
58
|
+
:title => 'Audio >> Lossy Audio'
|
59
|
+
},
|
60
|
+
:pictures_all => {
|
61
|
+
:id => '4_0',
|
62
|
+
:title => 'Pictures'
|
63
|
+
},
|
64
|
+
:pictures_photos => {
|
65
|
+
:id => '4_17',
|
66
|
+
:title => 'Pictures >> Photos'
|
67
|
+
},
|
68
|
+
:pictures_graphics => {
|
69
|
+
:id => '4_18',
|
70
|
+
:title => 'Pictures >> Graphics'
|
71
|
+
},
|
72
|
+
:live_all => {
|
73
|
+
:id => '5_0',
|
74
|
+
:title => 'Live Action'
|
75
|
+
},
|
76
|
+
:live_raw => {
|
77
|
+
:id => '5_20',
|
78
|
+
:title => 'Live Action >> Raw Live Action'
|
79
|
+
},
|
80
|
+
:live_english => {
|
81
|
+
:id => '5_19',
|
82
|
+
:title => 'Live Action >> English-translated Live Action'
|
83
|
+
},
|
84
|
+
:live_nonenglish => {
|
85
|
+
:id => '5_21',
|
86
|
+
:title => 'Live Action >> Non-English-translated Live Action'
|
87
|
+
},
|
88
|
+
:live_promo => {
|
89
|
+
:id => '5_22',
|
90
|
+
:title => 'Live Action >> Live Action Promotional Video'
|
91
|
+
},
|
92
|
+
:software_all => {
|
93
|
+
:id => '6_0',
|
94
|
+
:title => 'Software'
|
95
|
+
},
|
96
|
+
:software_apps => {
|
97
|
+
:id => '6_23',
|
98
|
+
:title => 'Software >> Applications'
|
99
|
+
},
|
100
|
+
:software_games => {
|
101
|
+
:id => '6_24',
|
102
|
+
:title => 'Software >> Games'
|
103
|
+
},
|
104
|
+
}
|
105
|
+
|
106
|
+
FILS = {
|
107
|
+
:show_all => {
|
108
|
+
:id => '0',
|
109
|
+
:title => 'Show all'
|
110
|
+
},
|
111
|
+
:filter_remakes => {
|
112
|
+
:id => '1',
|
113
|
+
:title => 'Filter Remakes'
|
114
|
+
},
|
115
|
+
:trusted_only => {
|
116
|
+
:id => '2',
|
117
|
+
:title => 'Trusted only'
|
118
|
+
},
|
119
|
+
:aplus_only => {
|
120
|
+
:id => '3',
|
121
|
+
:title => 'A+ only'
|
122
|
+
},
|
123
|
+
}
|
124
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Nyaa
|
3
|
+
class Downloader
|
4
|
+
attr_accessor :target, :destination, :retries
|
5
|
+
attr_accessor :response, :filename
|
6
|
+
|
7
|
+
def initialize(url, path, retries = 3)
|
8
|
+
self.target = url
|
9
|
+
self.destination = sane_dir(path)
|
10
|
+
self.retries = retries
|
11
|
+
self.response = request
|
12
|
+
self.filename = name_from_disposition
|
13
|
+
|
14
|
+
@fail = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
unless @fail
|
19
|
+
File.open("#{self.destination}/#{filename}", 'w') do |f|
|
20
|
+
f.write(self.response.body)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def failed?
|
26
|
+
@fail
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def request
|
32
|
+
begin
|
33
|
+
response = RestClient.get(self.target)
|
34
|
+
rescue StandardError => e
|
35
|
+
if retries > 0
|
36
|
+
retries -= 1
|
37
|
+
sleep = 1
|
38
|
+
retry
|
39
|
+
end
|
40
|
+
@fail = true
|
41
|
+
end
|
42
|
+
@fail = false
|
43
|
+
response
|
44
|
+
end
|
45
|
+
|
46
|
+
# Filename from Content Disposition Header Field
|
47
|
+
# http://www.ietf.org/rfc/rfc2183.txt
|
48
|
+
def name_from_disposition
|
49
|
+
disp = self.response.headers[:content_disposition]
|
50
|
+
disp_filename = disp.split(/;\s+/).select { |v| v =~ /filename\s*=/ }[0]
|
51
|
+
re = /([""'])(?:(?=(\\?))\2.)*?\1/
|
52
|
+
if re.match(disp_filename)
|
53
|
+
filename = re.match(disp_filename).to_s.gsub(/\A['"]+|['"]+\Z/, "")
|
54
|
+
else
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def sane_dir(path)
|
60
|
+
path = Dir.pwd if path.nil? || !File.writable?(path)
|
61
|
+
FileUtils.mkdir_p path unless File.directory?(path)
|
62
|
+
path
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/nyaa/search.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Nyaa
|
3
|
+
class Search
|
4
|
+
attr_accessor :query, :category, :filter
|
5
|
+
attr_accessor :offset, :count, :results
|
6
|
+
attr_accessor :runid, :cachedir
|
7
|
+
|
8
|
+
def initialize(query, cat = nil, fil = nil)
|
9
|
+
self.query = URI.escape(query)
|
10
|
+
self.category = cat ? CATS[cat.to_sym][:id] : '0_0'
|
11
|
+
self.filter = fil ? FILS[fil.to_sym][:id] : '0'
|
12
|
+
self.offset = 0
|
13
|
+
self.results = []
|
14
|
+
self.count = 1.0/0.0
|
15
|
+
|
16
|
+
self.runid = Time.new.to_i
|
17
|
+
self.cachedir = File.expand_path('~/.nyaa/cache')
|
18
|
+
FileUtils.mkdir_p(cachedir) unless File.directory?(cachedir)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns current batch (page) results
|
22
|
+
def get_results
|
23
|
+
if self.offset.zero?
|
24
|
+
batch = []
|
25
|
+
elsif self.offset == 1
|
26
|
+
batch = self.results[0, 100]
|
27
|
+
else # self.offset > 1
|
28
|
+
batch = self.results[(self.offset - 1) * 100, 100]
|
29
|
+
end
|
30
|
+
batch
|
31
|
+
end
|
32
|
+
|
33
|
+
def more
|
34
|
+
self.offset += 1
|
35
|
+
if self.results.length < self.count
|
36
|
+
extract(self.offset)
|
37
|
+
else
|
38
|
+
self.results = []
|
39
|
+
puts "No more results"
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def cached(page)
|
45
|
+
cachefile = "#{self.cachedir}/cache_#{self.runid}_p#{page}"
|
46
|
+
p cachefile
|
47
|
+
return nil unless File.exists?(cachefile)
|
48
|
+
|
49
|
+
File.open(cachefile, 'rb') do |file|
|
50
|
+
begin
|
51
|
+
results = Marshal.load(file)
|
52
|
+
rescue => e
|
53
|
+
puts "ERROR: Failed to load #{cachefile}"
|
54
|
+
puts "#{e.backtrace}: #{e.message} (#{e.class})"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
results
|
58
|
+
end
|
59
|
+
|
60
|
+
def purge
|
61
|
+
FileUtils.rm_rf Dir.glob("#{self.cachedir}/*")
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def dump(page, results)
|
67
|
+
cachefile = "cache_#{self.runid}_p#{page}"
|
68
|
+
File.open("#{self.cachedir}/#{cachefile}", 'wb') do |file|
|
69
|
+
begin
|
70
|
+
Marshal.dump(results, file)
|
71
|
+
rescue => e
|
72
|
+
puts "ERROR: Failed to dump #{cachefile}"
|
73
|
+
puts "#{e.backtrace}: #{e.message} (#{e.class})"
|
74
|
+
ensure
|
75
|
+
file.close
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def dump_json(page, results)
|
81
|
+
cachefile = "cache_#{self.runid}_p#{page}.json"
|
82
|
+
File.open("#{self.cachedir}/#{cachefile}", 'w') do |file|
|
83
|
+
begin
|
84
|
+
batch = []
|
85
|
+
results.each { |r| batch << r.to_hash }
|
86
|
+
file.write(batch.to_json)
|
87
|
+
rescue => e
|
88
|
+
puts "ERROR: Failed to dump #{cachefile}"
|
89
|
+
puts "#{e.backtrace}: #{e.message} (#{e.class})"
|
90
|
+
ensure
|
91
|
+
file.close
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def extract(page)
|
97
|
+
raw = fetch(page)
|
98
|
+
doc = Nokogiri::HTML(raw)
|
99
|
+
self.count = doc.css('span.notice').text.match(/\d+/).to_s.to_i
|
100
|
+
rows = doc.css('div#main div.content table.tlist tr.tlistrow')
|
101
|
+
rows.each { |row| self.results << Torrent.new(row) }
|
102
|
+
dump(page, self.results)
|
103
|
+
#dump_json(page, self.results)
|
104
|
+
end
|
105
|
+
|
106
|
+
def fetch(page)
|
107
|
+
url = "#{BASE_URL}&offset=#{page}"
|
108
|
+
url << "&cats=#{self.category}&filter=#{self.filter}"
|
109
|
+
url << "&term=#{self.query}" unless self.query.empty?
|
110
|
+
open(url).read
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/nyaa/torrent.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Nyaa
|
3
|
+
|
4
|
+
class Torrent
|
5
|
+
attr_accessor :tid, :name, :info, :link
|
6
|
+
attr_accessor :filesize, :seeders, :leechers
|
7
|
+
attr_accessor :category, :status, :downloads, :comments
|
8
|
+
attr_accessor :health, :bytes
|
9
|
+
|
10
|
+
def initialize (row = nil)
|
11
|
+
self.tid = row.css('td.tlistname').at('a')['href'][/tid=\d+/].gsub(/\D/,'')
|
12
|
+
self.name = row.css('td.tlistname').at('a').text.strip
|
13
|
+
self.info = row.css('td.tlistname').at('a')['href']
|
14
|
+
self.link = row.css('td.tlistdownload').at('a')['href']
|
15
|
+
self.filesize = row.css('td.tlistsize').text
|
16
|
+
self.seeders = row.css('td.tlistsn').text.to_i
|
17
|
+
self.leechers = row.css('td.tlistln').text.to_i
|
18
|
+
|
19
|
+
self.status = state(row.values[0])
|
20
|
+
self.category = row.css('td.tlisticon').at('a')['title']
|
21
|
+
self.downloads = row.css('td.tlistdn').text
|
22
|
+
self.comments = row.css('td.tlistmn').text
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def health( leech_weight = 0.5, seed_weight = 1.0 )
|
27
|
+
seeders.zero? ? 0 : seeders * seed_weight + leechers * leech_weight
|
28
|
+
end
|
29
|
+
|
30
|
+
def bytes
|
31
|
+
match = filesize.match(/([\d.]+)(.*)/)
|
32
|
+
if match
|
33
|
+
raw_size = match[1].to_f
|
34
|
+
case match[2].strip
|
35
|
+
when /gib/i then raw_size * 1000000000
|
36
|
+
when /mib/i then raw_size * 1000000
|
37
|
+
when /kib/i then raw_size * 1000
|
38
|
+
else nil
|
39
|
+
end
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def state(value)
|
46
|
+
case value
|
47
|
+
when 'trusted tlistrow' then status = 'Trusted'
|
48
|
+
when 'remake tlistrow' then status = 'Remake'
|
49
|
+
when 'aplus tlistrow' then status = 'A+'
|
50
|
+
when 'tlistrow' then status = 'Normal'
|
51
|
+
else status = 'Normal'
|
52
|
+
end
|
53
|
+
status
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_hash
|
57
|
+
hash = {}
|
58
|
+
instance_variables.each do |var|
|
59
|
+
hash[var.to_s.delete("@")] = instance_variable_get(var)
|
60
|
+
end
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/nyaa/ui.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
include Curses
|
3
|
+
|
4
|
+
module Nyaa
|
5
|
+
class UI
|
6
|
+
attr_accessor :menusize, :page
|
7
|
+
|
8
|
+
def initialize (config, search)
|
9
|
+
@config = config
|
10
|
+
@status = { :text => 'Ready.', :type => :default }
|
11
|
+
setup_curses
|
12
|
+
|
13
|
+
# columns
|
14
|
+
@info_columns = %w[ Size SE LE ]
|
15
|
+
@info_column_width = 10
|
16
|
+
@name_column_width = 0
|
17
|
+
|
18
|
+
# commands
|
19
|
+
@commands = {
|
20
|
+
'?' => 'help',
|
21
|
+
'g' => 'get',
|
22
|
+
'i' => 'info',
|
23
|
+
'n' => 'next',
|
24
|
+
'p' => 'prev',
|
25
|
+
'q' => 'quit',
|
26
|
+
}
|
27
|
+
|
28
|
+
@search = search
|
29
|
+
@torrents = @search.more.results
|
30
|
+
@num_torrents = @torrents.size
|
31
|
+
harvester # start bg harvester
|
32
|
+
|
33
|
+
@menusize = lines - 4 # lines - header + footer + status
|
34
|
+
@page = 1
|
35
|
+
@num_pages = (@search.count/@menusize.to_f).ceil
|
36
|
+
@offset = 0
|
37
|
+
# TODO: on hold state: prevent paging forward when waiting on results
|
38
|
+
end
|
39
|
+
|
40
|
+
def header
|
41
|
+
# pad info columns to column width
|
42
|
+
@info_columns = @info_columns.map do |column|
|
43
|
+
sprintf("%#{@info_column_width}s", column)
|
44
|
+
end
|
45
|
+
# key column width is whatever is leftover
|
46
|
+
@name_column_width = cols - (@info_columns.length * @info_column_width)
|
47
|
+
|
48
|
+
# header bar
|
49
|
+
cat = CATS[@config[:category].to_sym][:title]
|
50
|
+
header_text = sprintf " %-#{@name_column_width-1}s%s", "Nyaa - #{cat}", @info_columns.join
|
51
|
+
attrset(color_pair(1))
|
52
|
+
setpos(0,0)
|
53
|
+
addstr(sprintf "%-#{cols}s", header_text)
|
54
|
+
end
|
55
|
+
|
56
|
+
def footer
|
57
|
+
footer_text = @commands.map { |k,v| "#{k}: #{v}" }.join(' ')
|
58
|
+
attrset(color_pair(2))
|
59
|
+
setpos(lines - 1, 0)
|
60
|
+
addstr(sprintf " %-#{cols}s", footer_text)
|
61
|
+
|
62
|
+
search_summary = sprintf " %-14s %-14s %-14s",
|
63
|
+
"view: [#{@offset+1}-#{@offset+@menusize}]/#{@search.count}",
|
64
|
+
"recv: #{@num_torrents}/#{@search.count}",
|
65
|
+
"page: #{@page}/#{@num_pages}"
|
66
|
+
attrset(color_pair(2))
|
67
|
+
setpos(lines - 2, 0)
|
68
|
+
addstr(sprintf "%-#{cols}s", search_summary)
|
69
|
+
end
|
70
|
+
|
71
|
+
def status(text = nil, type = nil)
|
72
|
+
@status[:text] = text if text
|
73
|
+
@status[:type] = type
|
74
|
+
|
75
|
+
case @status[:type]
|
76
|
+
when :success then profile = 8
|
77
|
+
when :failure then profile = 9
|
78
|
+
else profile = 1
|
79
|
+
end
|
80
|
+
|
81
|
+
status_text = sprintf " Status: %-s", @status[:text]
|
82
|
+
attrset(color_pair(profile))
|
83
|
+
setpos(lines-3,0)
|
84
|
+
addstr(sprintf "%-#{cols}s", status_text)
|
85
|
+
refresh
|
86
|
+
end
|
87
|
+
|
88
|
+
def menu(highlight)
|
89
|
+
xpos = 1
|
90
|
+
attrset(color_pair(0))
|
91
|
+
setpos(xpos, 0)
|
92
|
+
|
93
|
+
(0..@menusize-1).each do |i|
|
94
|
+
if i < @search.count - @offset
|
95
|
+
if @torrents[@offset + i].nil?
|
96
|
+
status("Fetching more results, try again.", :failure)
|
97
|
+
else
|
98
|
+
line_text = sprintf("% -#{@name_column_width}s %9s %9s %9s",
|
99
|
+
truncate("#{@torrents[@offset + i].name}", @name_column_width),
|
100
|
+
@torrents[@offset + i].filesize,
|
101
|
+
@torrents[@offset + i].seeders,
|
102
|
+
@torrents[@offset + i].leechers)
|
103
|
+
|
104
|
+
attrset(color_pair(torrent_status(@torrents[@offset + i])))
|
105
|
+
setpos(xpos, 0)
|
106
|
+
# highlight the present choice
|
107
|
+
if highlight == i + 1
|
108
|
+
attron(A_STANDOUT)
|
109
|
+
addstr(line_text)
|
110
|
+
attroff(A_STANDOUT)
|
111
|
+
else
|
112
|
+
addstr(line_text)
|
113
|
+
end
|
114
|
+
xpos += 1
|
115
|
+
end
|
116
|
+
else
|
117
|
+
# blank lines if there's < @menusize of results
|
118
|
+
line_text = " "*cols
|
119
|
+
addstr(line_text)
|
120
|
+
xpos += 1
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# TODO: Pop up help window
|
126
|
+
def help
|
127
|
+
end
|
128
|
+
|
129
|
+
def move(cursor, increment)
|
130
|
+
if increment < 0 # negative, retreat!
|
131
|
+
if cursor == 1
|
132
|
+
if @offset == 0
|
133
|
+
cursor = 1
|
134
|
+
else
|
135
|
+
prev_page
|
136
|
+
cursor = menusize
|
137
|
+
end
|
138
|
+
else
|
139
|
+
cursor = cursor + increment
|
140
|
+
end
|
141
|
+
else # non-negative, advance!
|
142
|
+
if cursor == menusize
|
143
|
+
next_page
|
144
|
+
cursor = 1
|
145
|
+
else
|
146
|
+
cursor = cursor + increment
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def get(choice)
|
152
|
+
torrent = @torrents[@offset + choice - 1]
|
153
|
+
download = Downloader.new(torrent.link, @config[:output])
|
154
|
+
download.save
|
155
|
+
unless download.failed?
|
156
|
+
status("Downloaded successful: #{torrent.tid}", :success)
|
157
|
+
else
|
158
|
+
status("Download failed (3 attempts): #{torrent.tid}", :failure)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def open(choice)
|
163
|
+
torrent = @torrents[@offset + choice - 1]
|
164
|
+
link = "#{torrent.info}"
|
165
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ then
|
166
|
+
system("start #{link}")
|
167
|
+
elsif RbConfig::CONFIG['host_os'] =~ /darwin/ then
|
168
|
+
system("open '#{link}'", [:out, :err]=>'/dev/null')
|
169
|
+
elsif RbConfig::CONFIG['host_os'] =~ /linux/ then
|
170
|
+
system("xdg-open '#{link}'", [:out, :err]=>'/dev/null')
|
171
|
+
end
|
172
|
+
status("Opened '#{link}'", :success)
|
173
|
+
end
|
174
|
+
|
175
|
+
def next_page
|
176
|
+
status("Ready.")
|
177
|
+
unless @page + 1 > @num_pages
|
178
|
+
@page += 1
|
179
|
+
end
|
180
|
+
|
181
|
+
unless @offset + @menusize > @num_torrents
|
182
|
+
@offset += @menusize
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def prev_page
|
187
|
+
unless @page - 1 < 1
|
188
|
+
@page += -1
|
189
|
+
end
|
190
|
+
|
191
|
+
unless @offset == 0
|
192
|
+
@offset -= @menusize
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def resize_handler(cursor)
|
197
|
+
@menusize = lines - 4
|
198
|
+
menu(cursor)
|
199
|
+
refresh
|
200
|
+
end
|
201
|
+
|
202
|
+
def harvester
|
203
|
+
Thread.new do
|
204
|
+
until @num_torrents == @search.count
|
205
|
+
@torrents = @search.more.results
|
206
|
+
@num_torrents = @torrents.size
|
207
|
+
sleep 2
|
208
|
+
end
|
209
|
+
Thread.kill
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def setup_curses
|
216
|
+
noecho # don't show typed keys
|
217
|
+
init_screen # start curses mode
|
218
|
+
cbreak # disable line buffering
|
219
|
+
curs_set(0) # make cursor invisible
|
220
|
+
stdscr.keypad(true) # enable arrow keys
|
221
|
+
|
222
|
+
# set keyboard input timeout - sneaky way to manage refresh rate
|
223
|
+
timeout = 500 # milliseconds
|
224
|
+
|
225
|
+
if can_change_color?
|
226
|
+
start_color
|
227
|
+
init_pair(0, COLOR_WHITE, COLOR_BLACK) # ui:default
|
228
|
+
init_pair(1, COLOR_BLACK, COLOR_CYAN) # ui:header
|
229
|
+
init_pair(2, COLOR_BLACK, COLOR_YELLOW) # ui:footer
|
230
|
+
init_pair(3, COLOR_MAGENTA, COLOR_BLACK) # ui:selected
|
231
|
+
|
232
|
+
init_pair(4, COLOR_WHITE, COLOR_BLACK) # torrent:normal
|
233
|
+
init_pair(5, COLOR_GREEN, COLOR_BLACK) # torrent:trusted
|
234
|
+
init_pair(6, COLOR_BLUE, COLOR_BLACK) # torrent:aplus
|
235
|
+
init_pair(7, COLOR_RED, COLOR_BLACK) # torrent:remake
|
236
|
+
|
237
|
+
init_pair(8, COLOR_BLACK, COLOR_GREEN) # type:success
|
238
|
+
init_pair(9, COLOR_BLACK, COLOR_RED) # type:failure
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def torrent_status(torrent)
|
243
|
+
case torrent.status
|
244
|
+
when 'Trusted' then 5
|
245
|
+
when 'A+' then 6
|
246
|
+
when 'Remake' then 7
|
247
|
+
else 4
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def truncate(text, width)
|
252
|
+
if text.length > width
|
253
|
+
truncated = "#{text[0..width-4]}..."
|
254
|
+
else
|
255
|
+
text
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
end # UI
|
260
|
+
end # Nyaa
|