kemonomachi-wanko 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +14 -0
- data/README +49 -0
- data/bin/wanko +8 -0
- data/lib/wanko.rb +7 -0
- data/lib/wanko/command.rb +73 -0
- data/lib/wanko/data.rb +70 -0
- data/lib/wanko/exception.rb +8 -0
- data/lib/wanko/fetch.rb +178 -0
- data/lib/wanko/read.rb +88 -0
- data/lib/wanko/utility.rb +59 -0
- data/lib/wanko/wanko.rb +111 -0
- data/lib/wanko/write.rb +27 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c386e5f07a045d367717650f2f6fe54e2223b080
|
4
|
+
data.tar.gz: 90358f22e960700a46024b874a129cee3f2bfe17
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f70b0fd6c5fff57da833b5599a756f1256e4f55a38bdb1b15637d203b541ec8f82a62df6379c8d73281de4260ade511f9aa29cfa6862d0830818a1748566029
|
7
|
+
data.tar.gz: 88b87e0b9b1f8f1b86d91cb78f879be33f52a56cdc62b7dbf34105c02f7dc3ad6bccc652270a7d528d0897dc79e2c71b50896e89385cdafb9586509e8489ae7e
|
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
14
|
+
|
data/README
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
Wanko - RSS Torrent Fetcher
|
2
|
+
|
3
|
+
Description
|
4
|
+
-----------
|
5
|
+
|
6
|
+
Fetches torrent file links from RSS feeds based on user-specified rules. The
|
7
|
+
links can be printed to stdout or sent to Transmission for download, or the
|
8
|
+
torrent files can be downloaded to a watchdir for processing by other torrent
|
9
|
+
clients.
|
10
|
+
|
11
|
+
Installation
|
12
|
+
------------
|
13
|
+
|
14
|
+
wanko can be installed from RubyGems:
|
15
|
+
|
16
|
+
gem install kemonomachi+wanko
|
17
|
+
|
18
|
+
'kemonomachi+' is used as a ghetto namespace, since RubyGems doesn't support
|
19
|
+
proper ones.
|
20
|
+
|
21
|
+
Usage
|
22
|
+
-----
|
23
|
+
|
24
|
+
wanko [options]
|
25
|
+
|
26
|
+
The optional -c switch specifies the configuration directory, which defaults to
|
27
|
+
$HOME/.wanko.
|
28
|
+
|
29
|
+
See 'wanko --help' for usage info.
|
30
|
+
|
31
|
+
Can be run manually, but creating a cron job to run it at regular intervals is
|
32
|
+
recommended. It is not a daemon.
|
33
|
+
|
34
|
+
Configuration
|
35
|
+
-------------
|
36
|
+
|
37
|
+
The config file is named 'config.yaml'. The example folder contains a full
|
38
|
+
example config. RSS feeds, default fetch directory, fetch method and rules can
|
39
|
+
be specified. Regexen are used to match torrents against rules.
|
40
|
+
|
41
|
+
The config directory also holds 'history.yaml', which contains already read RSS
|
42
|
+
items. This prevents fetching the same torrent multiple times.
|
43
|
+
|
44
|
+
About the Name
|
45
|
+
--------------
|
46
|
+
|
47
|
+
Wanko (わんこ) is Japanese for 'doggy'. Because it's small and it fetches
|
48
|
+
stuff for you.
|
49
|
+
|
data/bin/wanko
ADDED
data/lib/wanko.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'terminal-table'
|
2
|
+
|
3
|
+
require 'wanko/read'
|
4
|
+
require 'wanko/wanko'
|
5
|
+
require 'wanko/write'
|
6
|
+
|
7
|
+
module Wanko
|
8
|
+
module Command
|
9
|
+
def self.fetch(options)
|
10
|
+
config = begin
|
11
|
+
Wanko::Read.config options[:config_dir]
|
12
|
+
rescue Errno::ENOENT
|
13
|
+
abort 'Config file not found, aborting...'
|
14
|
+
end
|
15
|
+
|
16
|
+
[:feeds, :rules].each do |key|
|
17
|
+
warn 'WARN: No #{key} specified.' if config[key].empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
history = Wanko::Read.history options[:config_dir]
|
21
|
+
|
22
|
+
torrents, new_history = Wanko::check_feeds config[:feeds], config[:rules], history
|
23
|
+
|
24
|
+
config[:fetcher].call torrents
|
25
|
+
|
26
|
+
Wanko::Write.history options[:config_dir], new_history
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.add(options)
|
30
|
+
config = begin
|
31
|
+
Wanko::Read.raw_config options[:config_dir]
|
32
|
+
rescue Errno::ENOENT
|
33
|
+
abort 'Config file not found, aborting...'
|
34
|
+
end
|
35
|
+
|
36
|
+
id = if config[:rules].empty?
|
37
|
+
0
|
38
|
+
else
|
39
|
+
config[:rules].map {|x| x[:id]}.max + 1
|
40
|
+
end
|
41
|
+
|
42
|
+
rule = [id: id, regex: options[:regex], dir: options[:dir] || config[:base_dir]]
|
43
|
+
|
44
|
+
new_config = config.merge rules: config[:rules] + rule
|
45
|
+
|
46
|
+
Wanko::Write.config options[:config_dir], Wanko::Utility.stringify_keys(new_config)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.list(options)
|
50
|
+
config = begin
|
51
|
+
Wanko::Read.raw_config options[:config_dir]
|
52
|
+
rescue Errno::ENOENT
|
53
|
+
abort 'Config file not found, aborting...'
|
54
|
+
end
|
55
|
+
|
56
|
+
puts Terminal::Table.new(rows: config[:rules].map {|r| [r[:id], r[:regex], r[:dir]]},
|
57
|
+
headings: ['ID', 'Regex', 'Directory']) {align_column 0, :right}
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.remove(options)
|
61
|
+
config = begin
|
62
|
+
Wanko::Read.raw_config options[:config_dir]
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
abort 'Config file not found, aborting...'
|
65
|
+
end
|
66
|
+
|
67
|
+
new_rules = config[:rules].reject {|rule| options[:ids].include? rule[:id]}
|
68
|
+
|
69
|
+
Wanko::Write.config options[:config_dir], Wanko::Utility.stringify_keys(config.merge rules: new_rules)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
data/lib/wanko/data.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Wanko
|
4
|
+
|
5
|
+
# Data representations.
|
6
|
+
module Data
|
7
|
+
|
8
|
+
# Class for torrent data. Contains the name of and the link to a torrent,
|
9
|
+
# as well as the directory the torrent will be downloaded to.
|
10
|
+
class Torrent
|
11
|
+
def initialize(name, link, dir)
|
12
|
+
@name = name
|
13
|
+
@link = link
|
14
|
+
@dir = dir
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :name, :link, :dir
|
18
|
+
|
19
|
+
# Returns a Hash representation of this Torrent
|
20
|
+
def to_h()
|
21
|
+
{
|
22
|
+
name: name,
|
23
|
+
link: link,
|
24
|
+
dir: dir
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a JSON representation of this Torrent
|
29
|
+
def to_json(state = nil)
|
30
|
+
to_h.to_json state
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Rule for matching against RSS items. Contains a regex for matching and a
|
35
|
+
# directory to download matched torrents to.
|
36
|
+
class Rule
|
37
|
+
|
38
|
+
# Public: Initialize a Rule object.
|
39
|
+
#
|
40
|
+
#regex - String or Regexp for matching.
|
41
|
+
#dir - Directory to download matched torrents to.
|
42
|
+
def initialize(regex, dir)
|
43
|
+
@regex = Regexp.new regex, Regexp::IGNORECASE
|
44
|
+
@dir = dir
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :regex, :dir
|
48
|
+
|
49
|
+
# Public: Match the regex of this Rule against a String
|
50
|
+
#
|
51
|
+
# str - String to match.
|
52
|
+
#
|
53
|
+
# Returns true if str matches, false otherwise.
|
54
|
+
def =~(str)
|
55
|
+
regex =~ str
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public: Compare this Rule to another object.
|
59
|
+
#
|
60
|
+
# other - Object to compare this Rule to.
|
61
|
+
#
|
62
|
+
# Returns true if the regex and dir fields of this Rule are equal to
|
63
|
+
# the corresponding fields in other, false otherwise.
|
64
|
+
def ==(other)
|
65
|
+
regex == other.regex && dir == other.dir
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/lib/wanko/fetch.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'wanko/exception'
|
7
|
+
require 'wanko/utility'
|
8
|
+
|
9
|
+
module Wanko
|
10
|
+
|
11
|
+
# Functions for fetching torrents in different ways. Several functions have
|
12
|
+
# side-effects, and ::fetcher_for returns Lambdas that have side-effects.
|
13
|
+
module Fetch
|
14
|
+
|
15
|
+
# Public: Create a lambda that fetches torrents when called.
|
16
|
+
#
|
17
|
+
# config - Hash with options for how to fetch torrents. :name is always
|
18
|
+
# required and should be one of 'stdout', 'watchdir' and
|
19
|
+
# 'transmission'. Other options vary depending on the value of
|
20
|
+
# :name, as follows:
|
21
|
+
#
|
22
|
+
# name: 'stdout'
|
23
|
+
# :format - Serialization format. See ::serialize for values.
|
24
|
+
# name: 'transmission'
|
25
|
+
# :host - The daemon's host
|
26
|
+
# :port - The daemon's port
|
27
|
+
# :path - The daemon's path.
|
28
|
+
# :user - Username for Basic auth (optional)
|
29
|
+
# :password - Password for Basic auth (optional)
|
30
|
+
#
|
31
|
+
# Returns a fetcher Lambda.
|
32
|
+
# Raises Wanko::ConfigError if :name option is not recognized.
|
33
|
+
def self.fetcher_for(config)
|
34
|
+
case config[:name]
|
35
|
+
when 'stdout'
|
36
|
+
->(torrents) {$stdout.puts serialize(config[:format], torrents)}
|
37
|
+
when 'watchdir'
|
38
|
+
->(torrents) {to_watchdir torrents}
|
39
|
+
when 'transmission'
|
40
|
+
url = make_transmission_url config[:host], config[:port], config[:path]
|
41
|
+
|
42
|
+
auth = if config[:user] || config[:password]
|
43
|
+
config.select {|k, _| [:user, :password].include? k}
|
44
|
+
end
|
45
|
+
|
46
|
+
->(torrents) {to_transmission url, auth, torrents}
|
47
|
+
else
|
48
|
+
raise Wanko::ConfigError, "Invalid fetcher name '#{config[:name]}'"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Internal: Serialize torrents for the stdout fetcher.
|
53
|
+
#
|
54
|
+
# format - Name of serialization method to use. Should be either 'json',
|
55
|
+
# 'simple' or 'yaml'.
|
56
|
+
# torrents - Torrents to serialize.
|
57
|
+
#
|
58
|
+
# Returns a String representation of torrents, in the given format.
|
59
|
+
# Raises Wanko::ConfigError if format is not recognized.
|
60
|
+
def self.serialize(format, torrents)
|
61
|
+
case format
|
62
|
+
when 'json'
|
63
|
+
torrents.to_json
|
64
|
+
when 'simple'
|
65
|
+
torrents.map(&:link).join "\n"
|
66
|
+
when 'yaml', nil
|
67
|
+
Utility.stringify_keys(torrents.map &:to_h).to_yaml
|
68
|
+
else
|
69
|
+
raise Wanko::ConfigError, "Invalid format '#{format}' for stdout fetcher."
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Internal: Download torrent files to watch directories. Creates any
|
74
|
+
# nonexisting directories.
|
75
|
+
#
|
76
|
+
# torrents - Torrents to download.
|
77
|
+
#
|
78
|
+
# Returns nothing.
|
79
|
+
def self.to_watchdir(torrents)
|
80
|
+
torrents.each do |tor|
|
81
|
+
FileUtils.mkdir_p tor.dir
|
82
|
+
|
83
|
+
open tor.link do |remote|
|
84
|
+
File.binwrite File.join(tor.dir, "#{tor.name}.torrent"), remote.read
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Internal: Send RPC commands to Transmission daemon for downloading
|
90
|
+
# torrents.
|
91
|
+
#
|
92
|
+
# url - URI object containing the URL of the daemon
|
93
|
+
# auth - Hash with :user and :password for Basic auth, or falsey value
|
94
|
+
# for no authorization.
|
95
|
+
# torrents - Array of torrents to download.
|
96
|
+
#
|
97
|
+
# Returns nothing.
|
98
|
+
def self.to_transmission(url, auth, torrents)
|
99
|
+
return if torrents.empty?
|
100
|
+
|
101
|
+
requests = torrents.map {|torrent| make_post_request url, make_transmission_rpc_command(torrent), auth}
|
102
|
+
|
103
|
+
Net::HTTP.start url.host, url.port do |transmission|
|
104
|
+
send_transmission_requests transmission, requests
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Internal: Send requests to a Transmission daemon. If a 409 Conflict
|
109
|
+
# response is recieved, update the 'X-transmission-Session-Id' header of
|
110
|
+
# the request and resend it.
|
111
|
+
#
|
112
|
+
# transmission - Net::HTTP connection to a Transmission daemon.
|
113
|
+
# reqs - Net::HTTP::Post requests to send to Transmission.
|
114
|
+
#
|
115
|
+
# Returns nothing.
|
116
|
+
def self.send_transmission_requests(transmission, reqs)
|
117
|
+
session_id = ''
|
118
|
+
|
119
|
+
reqs.each do |req|
|
120
|
+
req['X-Transmission-Session-Id'] = session_id
|
121
|
+
|
122
|
+
response = transmission.request req
|
123
|
+
|
124
|
+
case response
|
125
|
+
when Net::HTTPConflict
|
126
|
+
session_id = response['X-Transmission-Session-Id']
|
127
|
+
redo
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Internal: Create a URI object for a Transmission daemon URL.
|
133
|
+
#
|
134
|
+
# host - Host part of the URL.
|
135
|
+
# port - Port part of the URL.
|
136
|
+
# path - Path part of the URL.
|
137
|
+
#
|
138
|
+
# Returns a URI object.
|
139
|
+
def self.make_transmission_url(host, port, path)
|
140
|
+
URI::HTTP.build host: host || '127.0.0.1',
|
141
|
+
port: port || 9091,
|
142
|
+
path: "#{path || '/transmission/'}rpc"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Internal: Generate a JSON-encoded 'torrent-add' RPC command that can be
|
146
|
+
# sent to a Transmission daemon.
|
147
|
+
#
|
148
|
+
# torrent - Torrent to generate command for.
|
149
|
+
#
|
150
|
+
# Returns a String RPC command.
|
151
|
+
def self.make_transmission_rpc_command(torrent)
|
152
|
+
JSON.generate(
|
153
|
+
{
|
154
|
+
"method" => "torrent-add",
|
155
|
+
"arguments" => {
|
156
|
+
"filename" => torrent.link,
|
157
|
+
"download-dir" => torrent.dir
|
158
|
+
}
|
159
|
+
}
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Internal: Create an HTTP POST request.
|
164
|
+
#
|
165
|
+
# url - URL this request will be sent to.
|
166
|
+
# body - String with body data.
|
167
|
+
# auth - Hash with Basic auth data, or falsey value for no authorization.
|
168
|
+
#
|
169
|
+
# Returns a Net::HTTP::Post request
|
170
|
+
def self.make_post_request(url, body, auth)
|
171
|
+
request = Net::HTTP::Post.new url
|
172
|
+
request.body = body
|
173
|
+
request.basic_auth auth[:user], auth[:password] if auth
|
174
|
+
request
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
data/lib/wanko/read.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'wanko/data'
|
4
|
+
require 'wanko/fetch'
|
5
|
+
require 'wanko/utility'
|
6
|
+
|
7
|
+
module Wanko
|
8
|
+
|
9
|
+
# Functions to read in data from files, urls and similar.
|
10
|
+
# All functions can be considered to rely on external state.
|
11
|
+
module Read
|
12
|
+
|
13
|
+
# Public: Read a config file, create a fetcher lambda and convert the
|
14
|
+
# rules.
|
15
|
+
#
|
16
|
+
# dir - Path to the directory containing the config file.
|
17
|
+
#
|
18
|
+
# Returns a Hash containing the configuration.
|
19
|
+
def self.config(dir)
|
20
|
+
config = raw_config dir
|
21
|
+
|
22
|
+
config.merge(
|
23
|
+
fetcher: Fetch.fetcher_for({name: 'stdout'}.merge Hash config[:fetcher]),
|
24
|
+
rules: Array(config[:rules]).map {|rule| convert_rule rule, config[:base_dir]}
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Read a config file and supply some sensible defaults.
|
29
|
+
#
|
30
|
+
# dir - Path to the directory containing the config file.
|
31
|
+
#
|
32
|
+
# Returns a Hash containing the configuration.
|
33
|
+
def self.raw_config(dir)
|
34
|
+
config = Utility.symbolize_keys(YAML.load_file File.join(dir, 'config.yaml')) || {}
|
35
|
+
|
36
|
+
config.merge(
|
37
|
+
feeds: Array(config[:feeds]),
|
38
|
+
base_dir: config[:base_dir] || File.join(Dir.home, 'downloads'),
|
39
|
+
rules: Array(config[:rules])
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Internal: Convert a rule Hash into a Rule object, making the path
|
44
|
+
# absolute.
|
45
|
+
#
|
46
|
+
# rule - Rule to convert. Has a required :regex entry and an optional
|
47
|
+
# :dir entry.
|
48
|
+
# base_dir - Directory to use as base for relative paths, or as download
|
49
|
+
# dir if the rule has no :dir entry.
|
50
|
+
#
|
51
|
+
# Returns an Array of converted Rules.
|
52
|
+
def self.convert_rule(rule, base_dir)
|
53
|
+
Data::Rule.new rule[:regex], File.absolute_path(rule[:dir] || '', base_dir)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Read an RSS feed history file.
|
57
|
+
#
|
58
|
+
# dir - Path to the directory containing the history file.
|
59
|
+
#
|
60
|
+
# Returns a Hash containing the history, or an empty Hash if the file does
|
61
|
+
# not exist.
|
62
|
+
def self.history(dir)
|
63
|
+
history = begin
|
64
|
+
YAML.load_file File.join(dir, 'history.yaml')
|
65
|
+
rescue Errno::ENOENT
|
66
|
+
{}
|
67
|
+
end
|
68
|
+
|
69
|
+
# This is OK, since history is treated as read-only.
|
70
|
+
history.default = []
|
71
|
+
|
72
|
+
history
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Read an RSS feed.
|
76
|
+
#
|
77
|
+
# url - Location of the feed. Must be openable by OpenURI.
|
78
|
+
#
|
79
|
+
# Returns an RSS::Rss object, or nil if the feed couldn't be read.
|
80
|
+
def self.feed(url)
|
81
|
+
begin
|
82
|
+
open(url, read_timeout: 10) {|rss| RSS::Parser.parse rss}
|
83
|
+
rescue OpenURI::HTTPError, Timeout::Error, Errno::ECONNREFUSED, SocketError => ex
|
84
|
+
warn "WARN: #{url} --> #{ex}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Wanko
|
2
|
+
|
3
|
+
# Various utility functions.
|
4
|
+
module Utility
|
5
|
+
|
6
|
+
# Public: Recursively convert all keys of a Hash using the supplied block.
|
7
|
+
# Array values are traversed and each entry is converted. If arg is any
|
8
|
+
# other kind of object it is left untouched. If the conversion results in
|
9
|
+
# duplicate keys, later keys will overwrite earlier ones.
|
10
|
+
#
|
11
|
+
# Convenience functions exist for some often used operations, see
|
12
|
+
# ::symbolize_keys and ::stringify_keys.
|
13
|
+
#
|
14
|
+
# arg - Object to convert.
|
15
|
+
# block - Block for converting the keys. Will be called with each key
|
16
|
+
# individually. Should return an object that can be used as a hash key.
|
17
|
+
#
|
18
|
+
# Examples
|
19
|
+
#
|
20
|
+
# Utility.convert_keys h, &:to_sym
|
21
|
+
#
|
22
|
+
# Utility.convert_keys(h) {|key| key.downcase.squeeze}
|
23
|
+
#
|
24
|
+
# Returns an object with converted hash keys.
|
25
|
+
def self.convert_keys(arg, &block)
|
26
|
+
case arg
|
27
|
+
when Hash
|
28
|
+
arg.each_with_object({}) { |(key, val), memo|
|
29
|
+
memo[yield(key)] = convert_keys(val, &block)
|
30
|
+
}
|
31
|
+
when Array
|
32
|
+
arg.map {|val| convert_keys val, &block}
|
33
|
+
else
|
34
|
+
arg
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Recursively convert all keys in a Hash to Symbols. Does not check
|
39
|
+
# for duplicate keys. See ::convert_keys for more info.
|
40
|
+
#
|
41
|
+
# hash - Hash to convert. All keys must respond to #to_sym.
|
42
|
+
#
|
43
|
+
# Returns a structurally identical Hash with keys converted to Symbols.
|
44
|
+
def self.symbolize_keys(hash)
|
45
|
+
convert_keys hash, &:to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Recursively convert all keys in a Hash to Strings. Does not check
|
49
|
+
# for duplicate keys. See ::convert_keys for more info.
|
50
|
+
#
|
51
|
+
# hash - Hash to convert. All keys must respond to #to_s.
|
52
|
+
#
|
53
|
+
# Returns a structurally identical Hash with keys converted to Strings.
|
54
|
+
def self.stringify_keys(hash)
|
55
|
+
convert_keys hash, &:to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
data/lib/wanko/wanko.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'rss'
|
3
|
+
|
4
|
+
require 'wanko/command'
|
5
|
+
require 'wanko/data'
|
6
|
+
require 'wanko/read'
|
7
|
+
|
8
|
+
module Wanko
|
9
|
+
|
10
|
+
# Public: Parse cli switches. Exits and prints a usage message when given
|
11
|
+
# -h, --help or an invalid switch.
|
12
|
+
#
|
13
|
+
# args - Array of switches to parse. Will not be altered.
|
14
|
+
#
|
15
|
+
# Returns a Hash containing the parsed options.
|
16
|
+
def self.parse_cli_switches(args)
|
17
|
+
options = {
|
18
|
+
command: Command.method(:fetch),
|
19
|
+
config_dir: File.join(Dir.home, ".wanko")
|
20
|
+
}
|
21
|
+
|
22
|
+
parser = OptionParser.new do |parser|
|
23
|
+
parser.banner = 'Usage: wanko [options]'
|
24
|
+
|
25
|
+
parser.separator ''
|
26
|
+
parser.separator 'Options:'
|
27
|
+
|
28
|
+
parser.on '-c DIR', '--config_dir', 'Use a different config directory' do |dir|
|
29
|
+
options[:config_dir] = File.absolute_path dir
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.on '-a REGEX', '--add', 'Add a fetch rule' do |regex|
|
33
|
+
options[:command] = Command.method :add
|
34
|
+
options[:regex] = regex
|
35
|
+
end
|
36
|
+
|
37
|
+
parser.on '-d DIR', '--directory', 'Optional directory for fetch rules added with -a' do |dir|
|
38
|
+
options[:dir] = dir
|
39
|
+
end
|
40
|
+
|
41
|
+
parser.on '-l', '--list', 'List all rules' do
|
42
|
+
options[:command] = Command.method :list
|
43
|
+
end
|
44
|
+
|
45
|
+
parser.on '-r ID', '--remove', Integer, 'Remove a fetch rule' do |id|
|
46
|
+
options[:command] = Command.method :remove
|
47
|
+
options[:ids] = [id]
|
48
|
+
end
|
49
|
+
|
50
|
+
parser.separator ''
|
51
|
+
parser.separator 'Other:'
|
52
|
+
|
53
|
+
parser.on '-h', '--help',
|
54
|
+
'Show this message' do
|
55
|
+
puts parser
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
begin
|
61
|
+
parser.parse args
|
62
|
+
rescue OptionParser::InvalidOption
|
63
|
+
puts parser
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
|
67
|
+
options
|
68
|
+
end
|
69
|
+
|
70
|
+
# Public: Check RSS feeds for new torrents matching a set of rules, excluding
|
71
|
+
# already read items.
|
72
|
+
#
|
73
|
+
# urls - Array of feed urls to check.
|
74
|
+
# rules - Array of Rules to match against.
|
75
|
+
# history - Hash with urls mapped to Arrays of already read items. Will be
|
76
|
+
# accessed using the urls in the urls parameter. If a url is
|
77
|
+
# missing, an empty Array will be substituted.
|
78
|
+
#
|
79
|
+
# Returns a pair [[Torrent], Hash] of matched items and an updated history
|
80
|
+
def self.check_feeds(urls, rules, history)
|
81
|
+
matches, new_history = urls.map { |url|
|
82
|
+
feed = Read.feed(url) or next [[], {url => history[url]}]
|
83
|
+
|
84
|
+
[
|
85
|
+
match(feed, rules, Array(history[url])),
|
86
|
+
{url => feed.items.map {|item| item.guid.content}}
|
87
|
+
]
|
88
|
+
}.transpose
|
89
|
+
|
90
|
+
[matches.flatten, new_history.reduce(:merge)]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Internal: Match a set of rules against the items of an RSS feed, excluding
|
94
|
+
# already read items.
|
95
|
+
#
|
96
|
+
# feed - RSS object which items to search through.
|
97
|
+
# rules - Array of Rules.
|
98
|
+
# history - Array of GUIDs. Items found in this array will be rejected.
|
99
|
+
#
|
100
|
+
# Returns an Array of Torrents representing the matched items.
|
101
|
+
def self.match(feed, rules, history)
|
102
|
+
feed.items
|
103
|
+
.reject {|item| history.include? item.guid.content}
|
104
|
+
.product(rules)
|
105
|
+
.select {|item, rule| rule =~ item.title}
|
106
|
+
.map { |item, rule|
|
107
|
+
Data::Torrent.new item.title, item.link, rule.dir
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
data/lib/wanko/write.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Wanko
|
4
|
+
|
5
|
+
# Functions for writing data out to files or similar. All functions can be
|
6
|
+
# considered to have destructive side-effects.
|
7
|
+
module Write
|
8
|
+
|
9
|
+
# Public: Write a YAML representation of an object to a file named
|
10
|
+
# 'history.yaml'.
|
11
|
+
#
|
12
|
+
# This function _will_ clobber an existing file.
|
13
|
+
#
|
14
|
+
# dir - Path of the directory to write the file in.
|
15
|
+
# history - Object to write out. Responds to #to_yaml.
|
16
|
+
#
|
17
|
+
# Returns nothing
|
18
|
+
def self.history(dir, history)
|
19
|
+
File.write File.join(dir, 'history.yaml'), history.to_yaml
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.config(dir, config)
|
23
|
+
File.write File.join(dir, 'config.yaml'), config.to_yaml
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kemonomachi-wanko
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ookami Kenrou
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: terminal-table
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
description: |2
|
28
|
+
Fetches torrent file links from RSS feeds based on user-specified rules. The
|
29
|
+
links can be printed to stdout or sent to Transmission for download, or the
|
30
|
+
torrent files can be downloaded to a watchdir for processing by other torrent
|
31
|
+
clients.
|
32
|
+
email: ookamikenrou@gmail.com
|
33
|
+
executables:
|
34
|
+
- wanko
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- LICENSE
|
39
|
+
- README
|
40
|
+
- bin/wanko
|
41
|
+
- lib/wanko.rb
|
42
|
+
- lib/wanko/command.rb
|
43
|
+
- lib/wanko/data.rb
|
44
|
+
- lib/wanko/exception.rb
|
45
|
+
- lib/wanko/fetch.rb
|
46
|
+
- lib/wanko/read.rb
|
47
|
+
- lib/wanko/utility.rb
|
48
|
+
- lib/wanko/wanko.rb
|
49
|
+
- lib/wanko/write.rb
|
50
|
+
homepage: https://github.com/kemonomachi/wanko
|
51
|
+
licenses:
|
52
|
+
- WTFPL
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.0.0
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements:
|
69
|
+
- Transmission bittorrent client (optional)
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 2.5.1
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: RSS Torrent Fetcher
|
75
|
+
test_files: []
|