transmission-rss 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +56 -0
- data/bin/transmission-rss +115 -0
- data/lib/transmission-rss.rb +9 -0
- data/lib/transmission-rss/aggregator.rb +107 -0
- data/lib/transmission-rss/client.rb +92 -0
- data/lib/transmission-rss/config.rb +32 -0
- data/lib/transmission-rss/hash.rb +12 -0
- data/lib/transmission-rss/log.rb +44 -0
- metadata +77 -0
data/README.rdoc
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
= transmission-rss
|
2
|
+
|
3
|
+
transmission-rss is basically a workaround for transmission's lack of the
|
4
|
+
ability to monitor RSS feeds and automatically add enclosed torrent links.
|
5
|
+
|
6
|
+
As it's done with poems, I devote this very artful and romantic piece of code to
|
7
|
+
the single most delightful human being: Ann.
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
=== Latest stable version from rubygems.org
|
12
|
+
|
13
|
+
gem install transmission-rss
|
14
|
+
|
15
|
+
=== From source
|
16
|
+
|
17
|
+
gem build transmission-rss.gemspec
|
18
|
+
sudo gem install transmission-rss-*.gem
|
19
|
+
|
20
|
+
== Configuration
|
21
|
+
|
22
|
+
A yaml formatted config file is expected at +/etc/transmission-rss.conf+.
|
23
|
+
|
24
|
+
=== Minimal example
|
25
|
+
|
26
|
+
It should at least contain a list of feeds:
|
27
|
+
|
28
|
+
feeds:
|
29
|
+
- http://example.com/feed1
|
30
|
+
- http://example.com/feed2
|
31
|
+
|
32
|
+
=== All available options
|
33
|
+
|
34
|
+
The following configuration file example contains every existing option
|
35
|
+
(although +rss_check_interval+, +paused+ and +server+ are default values
|
36
|
+
and coult be omitted). The default +log_target+ is STDERR.
|
37
|
+
|
38
|
+
feeds:
|
39
|
+
- http://example.com/feed1
|
40
|
+
- http://example.com/feed2
|
41
|
+
|
42
|
+
rss_check_interval: 600
|
43
|
+
|
44
|
+
paused: false
|
45
|
+
|
46
|
+
server:
|
47
|
+
host: localhost
|
48
|
+
port: 9091
|
49
|
+
|
50
|
+
log_target: /var/log/transmissiond-rss.log
|
51
|
+
|
52
|
+
== TODO
|
53
|
+
|
54
|
+
* Timeout and error handling for aggregation and transmission communication
|
55
|
+
* Drop privileges
|
56
|
+
* Option to stop seeding after full download
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require( 'getoptlong' )
|
4
|
+
|
5
|
+
$:.unshift( File.join( File.dirname( __FILE__ ), '../lib' ) )
|
6
|
+
require( 'transmission-rss' )
|
7
|
+
|
8
|
+
include TransmissionRSS
|
9
|
+
|
10
|
+
# Default config file path.
|
11
|
+
configFile = '/etc/transmission-rss.conf'
|
12
|
+
|
13
|
+
# Do not fork by default.
|
14
|
+
dofork = false
|
15
|
+
|
16
|
+
# Default not verbose.
|
17
|
+
verbose = false
|
18
|
+
|
19
|
+
# Shows a summary of the command line options.
|
20
|
+
def usageMessage
|
21
|
+
$stderr << "#{File.basename( $0 )} [options]
|
22
|
+
Adds torrents from rss feeds to transmission web frontend.
|
23
|
+
|
24
|
+
-c <file> Custom config file path. Default: #{configFile}
|
25
|
+
-f Fork into background after startup.
|
26
|
+
-h This help.
|
27
|
+
-v Verbose mode.
|
28
|
+
|
29
|
+
"
|
30
|
+
exit( 1 )
|
31
|
+
end
|
32
|
+
|
33
|
+
# Define command-line options.
|
34
|
+
options = GetoptLong.new(
|
35
|
+
[ '-c', GetoptLong::REQUIRED_ARGUMENT ],
|
36
|
+
[ '-f', GetoptLong::NO_ARGUMENT ],
|
37
|
+
[ '-h', GetoptLong::NO_ARGUMENT ],
|
38
|
+
[ '-v', GetoptLong::NO_ARGUMENT ]
|
39
|
+
)
|
40
|
+
|
41
|
+
# Parse given options.
|
42
|
+
options.each do |option, argument|
|
43
|
+
case( option )
|
44
|
+
when '-c'
|
45
|
+
configFile = argument
|
46
|
+
when '-f'
|
47
|
+
dofork = true
|
48
|
+
when '-h'
|
49
|
+
usageMessage
|
50
|
+
when '-v'
|
51
|
+
verbose = true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Seems to be necessary when called from gem installation.
|
56
|
+
# Otherwise Config is somehow mixed up with RbConfig.
|
57
|
+
config = TransmissionRSS::Config.instance
|
58
|
+
|
59
|
+
# Default configuration.
|
60
|
+
config.load( {
|
61
|
+
'feeds' => [],
|
62
|
+
'rss_check_interval' => 600,
|
63
|
+
'paused' => false,
|
64
|
+
'server' => {
|
65
|
+
'host' => 'localhost',
|
66
|
+
'port' => 9091
|
67
|
+
},
|
68
|
+
'log_target' => $stderr
|
69
|
+
} )
|
70
|
+
|
71
|
+
# Initialize a log instance, configure it and run the consumer in a subthread.
|
72
|
+
log = Log.instance
|
73
|
+
log.verbose = verbose
|
74
|
+
log.target = config.log_target
|
75
|
+
tLog = Thread.start do log.run end
|
76
|
+
|
77
|
+
# Load config file (default or given by argument).
|
78
|
+
config.load( configFile )
|
79
|
+
log.add( config )
|
80
|
+
|
81
|
+
# Connect reload of config file to SIGHUP.
|
82
|
+
trap( 'HUP' ) do
|
83
|
+
config.load( configFile )
|
84
|
+
log.add( 'got hup', config )
|
85
|
+
end
|
86
|
+
|
87
|
+
# Initialize feed aggregator.
|
88
|
+
aggregator = Aggregator.new
|
89
|
+
|
90
|
+
# Initialize communication to transmission.
|
91
|
+
client = Client.new( config.server.host, config.server.port )
|
92
|
+
|
93
|
+
# Add feeds from config file to +Aggregator+ class.
|
94
|
+
aggregator.feeds.concat( config.feeds )
|
95
|
+
|
96
|
+
# Callback for a new item on one of the feeds.
|
97
|
+
aggregator.on_new_item do |torrentFile|
|
98
|
+
Thread.start do
|
99
|
+
client.addTorrent( torrentFile, config.paused )
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Start the aggregation process.
|
104
|
+
begin
|
105
|
+
if( dofork )
|
106
|
+
pid = fork do
|
107
|
+
aggregator.run( config.rss_check_interval )
|
108
|
+
end
|
109
|
+
|
110
|
+
puts( 'forked ' + pid.to_s )
|
111
|
+
else
|
112
|
+
aggregator.run( config.rss_check_interval )
|
113
|
+
end
|
114
|
+
rescue Interrupt
|
115
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require( 'etc' )
|
2
|
+
require( 'fileutils' )
|
3
|
+
require( 'open-uri' )
|
4
|
+
require( 'rss' )
|
5
|
+
|
6
|
+
# Class for aggregating torrent files through RSS feeds.
|
7
|
+
class TransmissionRSS::Aggregator
|
8
|
+
attr_accessor :feeds
|
9
|
+
|
10
|
+
def initialize( feeds = [] )
|
11
|
+
@feeds = feeds
|
12
|
+
@seen = []
|
13
|
+
|
14
|
+
# Initialize log instance.
|
15
|
+
@log = Log.instance
|
16
|
+
|
17
|
+
# Declare callback for new items.
|
18
|
+
callback( :on_new_item )
|
19
|
+
|
20
|
+
# Generate path for seen torrents store file.
|
21
|
+
@seenfile = File.join(
|
22
|
+
Etc.getpwuid.dir,
|
23
|
+
'/.config/transmission/seen-torrents.conf'
|
24
|
+
)
|
25
|
+
|
26
|
+
# Make directories in path if they are not existing.
|
27
|
+
FileUtils.mkdir_p( File.dirname( @seenfile ) )
|
28
|
+
|
29
|
+
# Touch seen torrents store file.
|
30
|
+
if( not File.exists?( @seenfile ) )
|
31
|
+
FileUtils.touch( @seenfile )
|
32
|
+
end
|
33
|
+
|
34
|
+
# Open file, read torrent URLs and add to +@seen+.
|
35
|
+
open( @seenfile ).readlines.each do |line|
|
36
|
+
@seen.push( line.chomp )
|
37
|
+
@log.add( 'from seenfile ' + line.chomp )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get file enclosures from all feeds items and call on_new_item callback
|
42
|
+
# with torrent file URL as argument.
|
43
|
+
def run( interval = 600 )
|
44
|
+
@log.add( 'aggregator start' )
|
45
|
+
|
46
|
+
while( true )
|
47
|
+
feeds.each do |url|
|
48
|
+
@log.add( 'aggregate ' + url )
|
49
|
+
|
50
|
+
begin
|
51
|
+
content = open( url ).readlines.join( "\n" )
|
52
|
+
items = RSS::Parser.parse( content, false ).items
|
53
|
+
rescue
|
54
|
+
@log.add( 'retrieval error' )
|
55
|
+
next
|
56
|
+
end
|
57
|
+
|
58
|
+
items.each do |item|
|
59
|
+
item.links.each do |link|
|
60
|
+
link = link.href
|
61
|
+
|
62
|
+
if( not seen?( link ) )
|
63
|
+
on_new_item( link )
|
64
|
+
@log.add( 'on_new_item event ' + link )
|
65
|
+
|
66
|
+
add_seen( link )
|
67
|
+
# else
|
68
|
+
# @log.add( 'already seen ' + link )
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sleep( interval )
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# To add a link into the list of seen links.
|
79
|
+
def add_seen( link )
|
80
|
+
@seen.push( link )
|
81
|
+
|
82
|
+
File.open( @seenfile, 'w' ) do |file|
|
83
|
+
file.write( @seen.join( "\n" ) )
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# To test if a link is in the list of seen links.
|
88
|
+
def seen?( link )
|
89
|
+
@seen.include?( link )
|
90
|
+
end
|
91
|
+
|
92
|
+
# Method to define callback methods.
|
93
|
+
def callback( *names )
|
94
|
+
names.each do |name|
|
95
|
+
eval <<-EOF
|
96
|
+
@#{name} = false
|
97
|
+
def #{name}( *args, &block )
|
98
|
+
if( block )
|
99
|
+
@#{name} = block
|
100
|
+
elsif( @#{name} )
|
101
|
+
@#{name}.call( *args )
|
102
|
+
end
|
103
|
+
end
|
104
|
+
EOF
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require( 'net/http' )
|
2
|
+
require( 'json' )
|
3
|
+
require( 'timeout' )
|
4
|
+
|
5
|
+
# TODO
|
6
|
+
# * Why the hell do timeouts in getSessionID and addTorrent not work?!
|
7
|
+
|
8
|
+
# Class for communication with transmission utilizing the RPC web interface.
|
9
|
+
class TransmissionRSS::Client
|
10
|
+
def initialize( host, port )
|
11
|
+
@host = host
|
12
|
+
@port = port
|
13
|
+
|
14
|
+
@log = Log.instance
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get transmission session id by simple GET.
|
18
|
+
def getSessionID
|
19
|
+
@log.add( 'getSessionID called' )
|
20
|
+
|
21
|
+
get = Net::HTTP::Get.new(
|
22
|
+
'/transmission/rpc'
|
23
|
+
)
|
24
|
+
|
25
|
+
# retries = 3
|
26
|
+
# begin
|
27
|
+
# Timeout::timeout( 5 ) do
|
28
|
+
response = Net::HTTP.new( @host, @port ).start do |http|
|
29
|
+
http.request( get )
|
30
|
+
end
|
31
|
+
# end
|
32
|
+
# rescue Timeout::Error
|
33
|
+
# puts( 'timeout error exception' ) if( $verbose )
|
34
|
+
# if( retries > 0 )
|
35
|
+
# retries -= 1
|
36
|
+
# puts( 'getSessionID timeout. retry..' ) if( $verbose )
|
37
|
+
# retry
|
38
|
+
# else
|
39
|
+
# $stderr << "timeout http://#{@host}:#{@port}/transmission/rpc"
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
|
43
|
+
id = response.header['x-transmission-session-id']
|
44
|
+
|
45
|
+
@log.add( 'got session id ' + id )
|
46
|
+
|
47
|
+
id
|
48
|
+
end
|
49
|
+
|
50
|
+
# POST json packed torrent add command.
|
51
|
+
def addTorrent( torrentFile, paused = false )
|
52
|
+
@log.add( 'addTorrent called' )
|
53
|
+
|
54
|
+
post = Net::HTTP::Post.new(
|
55
|
+
'/transmission/rpc',
|
56
|
+
initheader = {
|
57
|
+
'Content-Type' => 'application/json',
|
58
|
+
'X-Transmission-Session-Id' => getSessionID
|
59
|
+
}
|
60
|
+
)
|
61
|
+
|
62
|
+
post.body = {
|
63
|
+
"method" => "torrent-add",
|
64
|
+
"arguments" => {
|
65
|
+
"paused" => paused,
|
66
|
+
"filename" => torrentFile
|
67
|
+
}
|
68
|
+
}.to_json
|
69
|
+
|
70
|
+
# retries = 3
|
71
|
+
# begin
|
72
|
+
# Timeout::timeout( 5 ) do
|
73
|
+
response = Net::HTTP.new( @host, @port ).start do |http|
|
74
|
+
http.request( post )
|
75
|
+
end
|
76
|
+
# end
|
77
|
+
# rescue Timeout::Error
|
78
|
+
# puts( 'timeout error exception' ) if( $verbose )
|
79
|
+
# if( retries > 0 )
|
80
|
+
# retries -= 1
|
81
|
+
# puts( 'addTorrent timeout. retry..' ) if( $verbose )
|
82
|
+
# retry
|
83
|
+
# else
|
84
|
+
# $stderr << "timeout http://#{@host}:#{@port}/transmission/rpc"
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
|
88
|
+
result = JSON.parse( response.body ).result
|
89
|
+
|
90
|
+
@log.add( 'addTorrent result: ' + result )
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require( 'singleton' )
|
2
|
+
require( 'yaml' )
|
3
|
+
|
4
|
+
# Class handles configuration parameters.
|
5
|
+
class TransmissionRSS::Config < Hash
|
6
|
+
# This is a singleton class.
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
# Merges a Hash or YAML file (containing a Hash) with itself.
|
10
|
+
def load( config )
|
11
|
+
if( config.class == Hash )
|
12
|
+
self.merge!( config )
|
13
|
+
return
|
14
|
+
end
|
15
|
+
|
16
|
+
if( not config.nil? )
|
17
|
+
self.merge_yaml!( config )
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Merge Config Hash with Hash in YAML file.
|
22
|
+
def merge_yaml!( path )
|
23
|
+
self.merge!( load_file( path ) )
|
24
|
+
end
|
25
|
+
|
26
|
+
# Load YAML file and work around tabs not working for identation.
|
27
|
+
def load_file( path )
|
28
|
+
YAML.load_stream(
|
29
|
+
File.new( path ).read.gsub( /\t/, ' ' )
|
30
|
+
).documents.first
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Hash
|
2
|
+
# If a method is missing it is interpreted as the key of the hash. If the
|
3
|
+
# method has an argument (for example by "method="), the key called "method"
|
4
|
+
# is set to the respective argument.
|
5
|
+
def method_missing( symbol, *args )
|
6
|
+
if( args.size == 0 )
|
7
|
+
self[ symbol.to_s ]
|
8
|
+
else
|
9
|
+
self[ symbol.to_s.slice( 0..-2 ) ] = args.first
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require( 'singleton' )
|
2
|
+
|
3
|
+
class TransmissionRSS::Log
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
attr_accessor :target, :verbose
|
7
|
+
|
8
|
+
def add( *args )
|
9
|
+
@buffer ||= []
|
10
|
+
|
11
|
+
# Add every arg to buffer.
|
12
|
+
args.each do |arg|
|
13
|
+
@buffer.push( arg.to_s )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
@target ||= $stderr
|
19
|
+
@buffer ||= []
|
20
|
+
|
21
|
+
# If verbose is not defined, it will be nil.
|
22
|
+
@verbose = (@verbose rescue nil)
|
23
|
+
|
24
|
+
# If +@target+ seems to be a file path, open the file and tranform
|
25
|
+
# +@target+ into an IO for the file.
|
26
|
+
if( @target.class != IO and @target.match( /(\/.*)+/ ) )
|
27
|
+
@target = File.open( @target, 'a' )
|
28
|
+
@target.sync = true
|
29
|
+
|
30
|
+
@verbose = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Loop, pop buffer and puts.
|
34
|
+
while( true )
|
35
|
+
line = @buffer.shift
|
36
|
+
|
37
|
+
if( @verbose and line )
|
38
|
+
@target.puts( line )
|
39
|
+
end
|
40
|
+
|
41
|
+
sleep( 0.5 )
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: transmission-rss
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 17
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 7
|
10
|
+
version: 0.0.7
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- henning mueller
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-08-22 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: |-
|
23
|
+
transmission-rss is basically a workaround for
|
24
|
+
transmission's lack of the ability to monitor RSS feeds and
|
25
|
+
automatically add enclosed torrent links. Devoted to Ann.
|
26
|
+
email: henning@orgizm.net
|
27
|
+
executables:
|
28
|
+
- transmission-rss
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- bin/transmission-rss
|
35
|
+
- lib/transmission-rss.rb
|
36
|
+
- lib/transmission-rss/log.rb
|
37
|
+
- lib/transmission-rss/aggregator.rb
|
38
|
+
- lib/transmission-rss/config.rb
|
39
|
+
- lib/transmission-rss/hash.rb
|
40
|
+
- lib/transmission-rss/client.rb
|
41
|
+
- README.rdoc
|
42
|
+
has_rdoc: true
|
43
|
+
homepage:
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.7
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Adds torrents from rss feeds to transmission web frontend.
|
76
|
+
test_files: []
|
77
|
+
|