nyaa 0.3.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  coverage
6
+ Gemfile.lock
6
7
  InstalledFiles
7
8
  lib/bundler/man
8
9
  pkg
@@ -16,3 +17,5 @@ tmp
16
17
  .yardoc
17
18
  _yardoc
18
19
  doc/
20
+
21
+ TODO
data/Gemfile CHANGED
@@ -1,2 +1,3 @@
1
- source 'http://rubygems.org'
1
+ source :rubygems
2
+
2
3
  gemspec
data/README.md CHANGED
@@ -1,29 +1,49 @@
1
1
  # Nyaa
2
2
 
3
- Nyaa is a CLI to NyaaTorrents. You can browse, search, and download. Nifty.
3
+ Nyaa is a CLI to Nyaa.eu. You can browse, search, and download. Nifty.
4
+
5
+ ## Features
6
+
7
+ * Ncurses interface
8
+ * Search by category, filter, page, and query
9
+ * Browsing with pagination
10
+ * Download it or open a browser window from the interface
11
+ * Nyaa status aware: (aplus, trusted, remake, etc.)
12
+ * Batch mode for scripts (first page only atm)
13
+ * Supports unicode characters (requires `libncursesw5-dev` and `ruby1.9`)
4
14
 
5
15
  ## Installation
6
16
 
17
+ Stable release:
18
+
7
19
  gem install nyaa
8
20
 
9
- ## Usage
21
+ Development release:
10
22
 
11
- To start browsing immediately, simply run `nyaa`. The default category is english anime.
23
+ git clone git://github.com/mistofvongola/nyaa.git
12
24
 
13
- ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_1.png)
25
+ ## Browser Usage
26
+
27
+ To browse, simply run `nyaa`. The default category is english anime.
14
28
 
15
- Nyaa supports all the aspects of search of the main site. You can search by category and/or filters. Nyaa also shows a summary of seeders, leechers, total filesize, and number of downloads. To download an item, simply enter the number of the result.
29
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/v1.0.0_browse.png)
16
30
 
17
- nyaa -c anime_english -f trusted_only 'guilty crown'
31
+ Nyaa supports all the aspects of search of the main site. You can search by category and/or filters. To download an item, highlight it, and type `g`. To open the description page in a browser, type `i`. A sample query:
18
32
 
19
- ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_2.png)
33
+ nyaa -f trusted_only psycho pass
34
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/v1.0.0_search.png)
20
35
 
21
36
  For a list of categories and filters, see `nyaa -h`.
22
37
 
23
- ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_3.png)
38
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/v1.0.0_help.png)
39
+
40
+ ## The old interface
41
+
42
+ The old nyaa interface is deprecated, but is still included. You can use the old interface using the `--classic` option.
43
+
44
+ ![](https://github.com/mistofvongola/nyaa/raw/master/screenshots/screenshot_1.png)
24
45
 
25
46
  ## Contributing
26
- 0. See HACKING file for style guidelines
27
47
  1. Fork it
28
48
  2. Create your feature branch (`git checkout -b my-new-feature`)
29
49
  3. Commit your changes (`git commit -am 'Added some feature'`)
@@ -32,8 +52,8 @@ For a list of categories and filters, see `nyaa -h`.
32
52
 
33
53
  ## License
34
54
 
35
- See LICENSE file.
55
+ MIT License. See LICENSE file for details.
36
56
 
37
57
  ## Authors
38
58
 
39
- See AUTHORS file.
59
+ David Palma
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ task :default => :test
6
6
 
7
7
  Rake::TestTask.new do |t|
8
8
  t.libs << 'lib/nyaa'
9
- t.test_files = FileList['test/lib/nyaa/*_test.rb']
9
+ t.test_files = FileList['spec/lib/nyaa/*_spec.rb']
10
10
  t.verbose = true
11
11
  end
12
12
 
@@ -17,19 +17,22 @@ task :gemspec do
17
17
  gemspec.validate
18
18
  end
19
19
 
20
- desc "Build gem locally"
21
20
  task :build => :gemspec do
22
21
  system "gem build #{gemspec.name}.gemspec"
23
22
  FileUtils.mkdir_p "pkg"
24
23
  FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg"
25
24
  end
26
25
 
27
- desc "Install gem locally"
28
26
  task :install => :build do
29
27
  system "gem install pkg/#{gemspec.name}-#{gemspec.version}"
30
28
  end
31
29
 
32
- desc "Clean automatically generated files"
30
+ desc "Purge the pkg directory"
33
31
  task :clean do
34
32
  FileUtils.rm_rf "pkg"
35
33
  end
34
+
35
+ desc "Publish gem to rubygems.org"
36
+ task :publish do
37
+ system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
38
+ end
data/bin/nyaa CHANGED
@@ -1,49 +1,78 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
4
+
3
5
  require 'nyaa'
4
- require 'trollop'
5
-
6
- opts = Trollop::options do
7
- version "nyaa v#{Nyaa::VERSION} (c) 2012 David Palma http://github.com/mistofvongola"
8
- banner <<-EOM
9
- The nyaa gem is a simple CLI browser for NyaaTorrents.
10
- Usage:
11
- nyaa [options] "my search"
12
-
13
- Categories:
14
- anime_all, anime_raw, anime_english, anime_nonenglish, anime_music_video
15
- books_all, books_raw, books_english, books_nonenglish
16
- audio_all, audio_lossless, audio_lossy
17
- pictures_all, pictures_photos, pictures_graphics,
18
- live_all, live_raw, live_english, live_nonenglish, live_promo
19
- software_all, software_apps, software_games
20
-
21
- Filters:
22
- show_all, filter_remakes, trusted_only, aplus_only
23
-
24
- Options:
25
- EOM
26
- opt :category, "Select a category to search in. See below for valid options.", :default => 'anime_english'
27
- opt :filter, "Select a filter for your search. See below for valid options.", :default => 'show_all'
28
- opt :outdir, "Select the download directory.", :default => File.expand_path('~/Downloads')
29
- opt :size, "Show <i> results at a time. Must be between 1 and 100.", :type => :int, :default => 4
30
- opt :page, "Start by showing the <i>th result page.", :type => :int, :default => 1
31
- opt :version, "Print the version and exit."
32
- opt :help, "Show this information and exit."
6
+ include Curses
7
+
8
+
9
+ @opts = Nyaa::CLI.parse(ARGV)
10
+ @search = Nyaa::Search.new(@opts[:query], @opts[:category], @opts[:filter])
11
+
12
+ def batch_mode
13
+ results = @search.more.get_results
14
+ results.each do |r|
15
+ puts "o #{r.name}"
16
+ puts "#{r.link}"
17
+ puts
18
+ end
19
+ exit
33
20
  end
34
21
 
35
- unless Nyaa::Browser::CATS.has_key?(opts[:category])
36
- Trollop::die :category, "is not a valid category"
22
+ def browser_mode
23
+ begin
24
+ nyaa = Nyaa::Browser.new(@opts, @search)
25
+ rescue Interrupt
26
+ puts "\nInterrupt received. Exiting."
27
+ exit
28
+ end
37
29
  end
38
- unless Nyaa::Browser::FILS.has_key?(opts[:filter])
39
- Trollop::die :filter, "is not a valid filter"
30
+
31
+ def curses_mode
32
+ begin
33
+ yield
34
+ ensure
35
+ nocbreak
36
+ close_screen
37
+ end
40
38
  end
41
- query = ARGV.join(' ')
42
-
43
- begin
44
- n = Nyaa::Browser.new query, opts
45
- n.search
46
- rescue Interrupt
47
- puts "\nInterrupt received. Exiting."
48
- exit
39
+
40
+ # Batch mode
41
+ if @opts[:batch]
42
+ batch_mode
49
43
  end
44
+
45
+ if @opts[:classic] # use old interface
46
+ browser_mode
47
+ else
48
+ nyaa = Nyaa::UI.new(@opts, @search)
49
+ cursor = 1
50
+
51
+ curses_mode do
52
+ #TODO: Gracefully handle window resizing
53
+ #Signal.trap('SIGWINCH', nyaa.status("Window size changed!", :failure))
54
+ loop do
55
+ nyaa.header
56
+ nyaa.status
57
+ nyaa.footer
58
+ nyaa.menu(cursor)
59
+ refresh
60
+
61
+ case getch
62
+ when Key::UP then cursor = nyaa.move(cursor, -1)
63
+ when Key::DOWN then cursor = nyaa.move(cursor, 1)
64
+ when 'k' then cursor = nyaa.move(cursor, -1)
65
+ when 'j' then cursor = nyaa.move(cursor, 1)
66
+ when '?' then nyaa.status('help not implemented!', :failure)
67
+ when 'g' then nyaa.get(cursor)
68
+ when 'i' then nyaa.open(cursor)
69
+ when 'n' then nyaa.next_page
70
+ when 'p' then nyaa.prev_page
71
+ when 'q' then @search.purge && break
72
+ #when Key::RESIZE then nyaa.status("Window size changed!", :failure)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ exit
data/lib/nyaa.rb CHANGED
@@ -1,5 +1,28 @@
1
- require 'nyaa/version'
1
+ # -*- encoding : utf-8 -*-
2
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
3
+ # stdlib
4
+ require 'optparse'
5
+ require 'open-uri'
6
+ require 'curses'
2
7
 
3
- module Nyaa
4
- autoload :Browser, 'nyaa/browser'
8
+ # third party
9
+ begin
10
+ require 'rubygems' # for ruby 1.8 compat
11
+ require 'nokogiri'
12
+ require 'rest_client'
13
+ require 'formatador'
14
+ rescue LoadError => e
15
+ puts "LoadError: #{e.message}"
5
16
  end
17
+
18
+ # internal api
19
+ require 'nyaa/version'
20
+ require 'nyaa/cli'
21
+ require 'nyaa/constants'
22
+ require 'nyaa/torrent'
23
+ require 'nyaa/search'
24
+
25
+ # internal tools
26
+ require 'nyaa/browser' # old ui
27
+ require 'nyaa/ui' # new ui
28
+ require 'nyaa/downloader'
data/lib/nyaa/browser.rb CHANGED
@@ -1,161 +1,98 @@
1
- require 'nokogiri'
2
- require 'rest_client'
3
- require 'formatador'
4
- require 'uri'
5
-
1
+ # -*- encoding : utf-8 -*-
6
2
  module Nyaa
7
3
  class Browser
8
- BASE_URL = 'http://www.nyaa.eu/?page=torrents'
9
- PSIZE = 100
10
- CATS = {
11
- 'anime_all' => '1_0',
12
- 'anime_raw' => '1_11',
13
- 'anime_english' => '1_37',
14
- 'anime_nonenglish' => '1_38',
15
- 'anime_music_video' => '1_32',
16
- 'books_all' => '2_0',
17
- 'books_raw' => '2_13',
18
- 'books_english' => '2_12',
19
- 'books_nonenglish' => '2_39',
20
- 'audio_all' => '3_0',
21
- 'audio_lossless' => '3_14',
22
- 'audio_lossy' => '3_15',
23
- 'pictures_all' => '4_0',
24
- 'pictures_photos' => '4_17',
25
- 'pictures_graphics' => '4_18',
26
- 'live_all' => '5_0',
27
- 'live_raw' => '5_20',
28
- 'live_english' => '5_19',
29
- 'live_nonenglish' => '5_21',
30
- 'live_promo' => '5_22',
31
- 'software_all' => '6_0',
32
- 'software_apps' => '6_23',
33
- 'software_games' => '6_24',
34
- }
35
- FILS = {
36
- 'show_all' => '0',
37
- 'filter_remakes' => '1',
38
- 'trusted_only' => '2',
39
- 'aplus_only' => '3',
40
- }
41
-
42
- def initialize(query, opts)
43
- @query = URI.escape(query)
4
+ def initialize(opts, search)
44
5
  @opts = opts
6
+ @opts[:size] = 4 if opts[:size].nil?
45
7
  @opts[:size] = PSIZE if opts[:size] > PSIZE
46
8
  @opts[:size] = 1 if opts[:size] <= 1
47
9
  @marker = 0
10
+ @format = Formatador.new
11
+ @page = 0 # Current browser page
12
+ @search = search
13
+ start
48
14
  end
49
15
 
50
- def search
51
- data = harvest(@query, @opts[:page])
52
- part = partition(data, 0, @opts[:size])
53
- display(data, part)
54
- end
55
-
56
- def harvest(query, page)
57
- url = "#{BASE_URL}"
58
- url << "&cats=#{CATS[@opts[:category]]}" if @opts[:category]
59
- url << "&filter=#{FILS[@opts[:filter]]}" if @opts[:filter]
60
- url << "&offset=#{page}" if @opts[:page]
61
- url << "&term=#{query}" unless @query.empty?
62
- doc = Nokogiri::HTML(RestClient.get(url))
63
- items = []
64
- rows = doc.css('div#main div.content table.tlist tr.tlistrow')
65
- #puts "DEBUG: Row: #{rows[0].to_s}"
66
- rows.each do |row|
67
- items << {
68
- :cat => row.css('td.tlisticon').at('a')['title'],
69
- :name => row.css('td.tlistname').at('a').text.strip,
70
- :dl => row.css('td.tlistdownload').at('a')['href'],
71
- :size => row.css('td.tlistsize').text,
72
- :se => row.css('td.tlistsn').text,
73
- :le => row.css('td.tlistln').text,
74
- :dls => row.css('td.tlistdn').text,
75
- :msg => row.css('td.tlistmn').text,
76
- # TODO: The status hashkey is broken
77
- :status =>
78
- if row.at('tr.trusted')
79
- 'trusted'
80
- elsif row.at('tr.remake')
81
- 'remake'
82
- elsif row.at('tr.aplus')
83
- 'aplus'
84
- else
85
- 'normal'
86
- end
87
- }
88
- end
89
- items
16
+ def start
17
+ #@search = Search.new(@opts[:query], @opts[:category], @opts[:filter])
18
+ page_results = @search.more.get_results
19
+ @page += 1
20
+ part = partition(page_results, 0, @opts[:size])
21
+ screen(page_results, part)
90
22
  end
91
23
 
92
24
  def partition(ary, start, size)
93
25
  start = 0 if start < 0
94
- # update marker
95
26
  @marker = start
96
27
  size = PSIZE if size > PSIZE
97
-
98
28
  part = ary[start, size]
29
+ part = [] if part.nil?
99
30
  part
100
31
  end
101
32
 
102
- def display(data, results)
103
- f = Formatador.new
104
- f.display_line( "\t[yellow]NyaaTorrents >> "\
105
- "Browse | Anime, manga, and music[/]\n" )
106
-
107
- if data[0].nil? || results[0].nil?
108
- f.display_line( "[normal]No matches found. "\
109
- "Try another category. See --help.[/]\n")
110
- f.display_line("\t[yellow]Exiting.[/]")
111
- exit
112
- end
113
- f.display_line( "[bold]#{data[0][:cat]}\n[/]" )
114
-
115
- results.each do |item|
116
- case item[:status]
117
- when 'aplus'
118
- flag = 'blue'
119
- when 'trusted'
120
- flag = 'green'
121
- when 'remake'
122
- flag = 'red'
123
- else
124
- flag = 'normal'
125
- end
126
- f.display_line( "[#{flag}]#{data.index(item)+1}. #{item[:name]}[/]")
127
-
128
- f.indent {
129
- f.display_line( "[bold]Size: [purple]#{item[:size]}[/] "\
130
- "[bold]SE: [green]#{item[:se]}[/] "\
131
- "[bold]LE: [red]#{item[:le]}[/] "\
132
- "[bold]DLs: [yellow]#{item[:dls]}[/] "\
133
- "[bold]Msg: [blue]#{item[:msg]}[/]" )
134
- f.display_line( "[green]#{item[:dl]}[/]" )
135
- }
33
+ def torrent_info(page_results, torrent)
34
+ case torrent.status
35
+ when 'A+' then flag = 'blue'
36
+ when 'Trusted' then flag = 'green'
37
+ when 'Remake' then flag = 'red'
38
+ else flag = 'yellow'
136
39
  end
137
40
 
41
+ @format.display_line("#{page_results.index(torrent)+1}. "\
42
+ "#{torrent.name[0..70]}[/]")
43
+ @format.indent {
44
+ @format.display_line(
45
+ "[bold]Size: [purple]#{torrent.filesize}[/] "\
46
+ "[bold]SE: [green]#{torrent.seeders}[/] "\
47
+ "[bold]LE: [red]#{torrent.leechers}[/] "\
48
+ "[bold]DLs: [yellow]#{torrent.downloads}[/] "\
49
+ "[bold]Msg: [blue]#{torrent.comments}[/]")
50
+ @format.display_line("[bold]DL:[/] [#{flag}]#{torrent.link}[/]")
51
+ }
52
+ end
53
+
54
+ def header_info
55
+ @format.display_line( "\t[yellow]NyaaTorrents >> "\
56
+ "Browse | Anime, manga, and music[/]\n" )
57
+ @format.display_line(
58
+ "[bold]#{CATS[@opts[:category].to_sym][:title]}\n[/]" )
59
+ end
60
+
61
+ def footer_info
138
62
  start_count = @marker + 1
139
63
  start_count = PSIZE if start_count > PSIZE
140
64
  end_count = @marker + @opts[:size]
141
65
  end_count = PSIZE if end_count > PSIZE
142
66
 
143
- f.display_line("\n\t[yellow]Displaying results "\
67
+ @format.display_line("\n\t[yellow]Displaying results "\
144
68
  "#{start_count} through #{end_count} of #{PSIZE} "\
145
- "#(Page #{@opts[:page]})\n")
69
+ "(Page ##{@page})\n")
70
+ end
71
+
72
+ def screen(page_results, screen_items)
73
+ header_info
74
+
75
+ if screen_items.empty?
76
+ @format.display_line( "[normal]End of results.")
77
+ @format.display_line("For more search options, see --help.[/]\n")
78
+ exit
79
+ end
80
+
81
+ screen_items.each do |torrent|
82
+ torrent_info(page_results, torrent)
83
+ end
146
84
 
147
- prompt(data, results)
85
+ footer_info
86
+ prompt(page_results, screen_items)
148
87
  end
149
88
 
150
- def prompt(data, results)
151
- f = Formatador.new
152
- f.display_line("[yellow]Help: q to quit, "\
89
+ def prompt(page_results, screen_items)
90
+ @format.display_line("[yellow]Help: q to quit, "\
91
+ "h for display help, "\
153
92
  "n/p for pagination, "\
154
93
  "or a number to download that choice.")
155
- # prompt
156
- f.display("[bold]>[/] ")
94
+ @format.display("[bold]>[/] ")
157
95
 
158
- # handle input
159
96
  choice = STDIN.gets
160
97
  if choice.nil?
161
98
  choice = ' '
@@ -164,48 +101,70 @@ module Nyaa
164
101
  end
165
102
 
166
103
  case
167
- when choice[0] == 'q'
168
- exit
169
- when choice[0] == 'n'
170
- if @marker + @opts[:size] == 100
171
- @opts[:page] += 1
172
- f.indent { f.display_line("[purple][blink_fast]! "\
173
- "Loading more results...[/]") }
174
- data = harvest(@query, @opts[:page])
175
- part = partition(data, 0, @opts[:size])
176
- else
177
- part = partition(data, @marker + @opts[:size], @opts[:size])
178
- end
179
- display(data, part)
180
- when choice[0] == 'p'
181
- if @marker < 1
182
- f.indent { f.display_line("[purple]! Already at page one.[/]") }
183
- input(data, results)
184
- else
185
- part = partition(data, @marker - @opts[:size], @opts[:size])
186
- display(data, part)
187
- end
188
- when choice[0].match(/\d/)
189
- /(\d+)(\s*\|(.*))*/.match(choice) do |str|
190
- num = str[1].to_i - 1
191
- download(data[num][:dl], @opts[:outdir])
192
- end
104
+ when choice[0] == 'q' then @search.purge && exit
105
+ when choice[0] == 'n' then paginate(page_results)
106
+ when choice[0] == 'p' then reverse_paginate(page_results, screen_items)
107
+ when choice[0].match(/\d/) then retrieve(choice, page_results, screen_items)
108
+ when choice[0] == 'h'
109
+ @format.display_line("[normal]The color of an entry's DL link "\
110
+ "represents its status:[/]")
111
+ @format.display_line("[blue]A+[/], [green]Trusted[/], "\
112
+ "[yellow]Normal[/], or [red]Remake[/]")
113
+ prompt(page_results, screen_items)
114
+ else
115
+ @format.display_line("[red]Unrecognized option.[/]")
116
+ prompt(page_results, screen_items)
193
117
  end
194
118
  end
195
119
 
196
- def download(url, output_path)
197
- resp = RestClient.get(url)
120
+ def paginate(page_results)
121
+ if @marker + @opts[:size] == 100
122
+ @format.display_line("[yellow]Loading more results...[/]")
123
+ if @page == @search.offset
124
+ page_results = @search.more.get_results
125
+ else # @page < @search.offset
126
+ page_results = @search.cached(@page + 1)
127
+ end
128
+ @page += 1
129
+ part = partition(page_results, 0, @opts[:size])
130
+ else
131
+ part = partition(page_results, @marker + @opts[:size], @opts[:size])
132
+ end
133
+ screen(page_results, part)
134
+ end
198
135
 
199
- # Get filename from Content-Disposition header
200
- disp_fname = resp.headers[:content_disposition].
201
- split(/;\s+/).
202
- select { |v| v =~ /filename\s*=/ }[0]
203
- local_fname = /([""'])(?:(?=(\\?))\2.)*?\1/.
204
- match(disp_fname).
205
- to_s.gsub(/\A['"]+|['"]+\Z/, "")
136
+ def reverse_paginate(page_results, screen_items)
137
+ if @marker < 1
138
+ if @page == 1
139
+ @format.display_line("[red]Already at page one.[/]")
140
+ prompt(page_results, screen_items)
141
+ else # @page > 1
142
+ @format.display_line("[yellow]Loading results...[/]")
143
+ # TODO Bug: This fails with --size 100
144
+ # TODO Bug: reverse paginate sometimes only returns p1 cache
145
+ page_results = @search.cached(@page - 1)
146
+ @page -= 1
147
+ part = partition(page_results, PSIZE - @opts[:size], @opts[:size])
148
+ screen(page_results, part)
149
+ end
150
+ else
151
+ part = partition(page_results, @marker - @opts[:size], @opts[:size])
152
+ screen(page_results, part)
153
+ end
154
+ end
206
155
 
207
- File.open("#{output_path}/#{local_fname}", 'w') do
208
- |f| f.write(resp.body)
156
+ def retrieve(choice, page_results, screen_items)
157
+ /(\d+)(\s*\|(.*))*/.match(choice) do |str|
158
+ num = str[1].to_i - 1
159
+ download = Downloader.new(page_results[num].link, @opts[:outdir])
160
+ download.save
161
+ unless download.failed?
162
+ @format.display_line(
163
+ "[green]Downloaded '#{download.filename}' successfully.[/]")
164
+ else
165
+ @format.display_line("[red]Download failed (3 attempts).[/]")
166
+ end
167
+ prompt(page_results, screen_items)
209
168
  end
210
169
  end
211
170
  end