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 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,9 @@
1
+ $:.unshift( File.dirname( __FILE__ ) )
2
+
3
+ module TransmissionRSS
4
+ VERSION = '0.0.7'
5
+ end
6
+
7
+ Dir.glob( $:.first + '/**/*.rb' ).each do |file|
8
+ require( file )
9
+ 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
+