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 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
+