transmission-rss 0.0.7
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/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
|
+
|