epodder 0.0.1
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/bin/epodder +6 -0
- data/lib/arguments.rb +57 -0
- data/lib/configuration/configuration.rb +4 -0
- data/lib/configuration/configurator.rb +90 -0
- data/lib/database/episode.rb +35 -0
- data/lib/database/podcast.rb +10 -0
- data/lib/eclass.rb +7 -0
- data/lib/epodder.rb +33 -0
- data/lib/verb/add.rb +35 -0
- data/lib/verb/catchup.rb +18 -0
- data/lib/verb/clean.rb +49 -0
- data/lib/verb/download.rb +43 -0
- data/lib/verb/fetch.rb +12 -0
- data/lib/verb/list.rb +14 -0
- data/lib/verb/remove.rb +18 -0
- data/lib/verb/update.rb +78 -0
- data/lib/verb/verb.rb +16 -0
- metadata +62 -0
data/bin/epodder
ADDED
data/lib/arguments.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Epodder
|
2
|
+
class Arguments
|
3
|
+
attr_accessor :args
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@args = Hash.new
|
7
|
+
@args[:arguments] = []
|
8
|
+
verb = Struct.new(:name,:desc,:block)
|
9
|
+
@verbs = [
|
10
|
+
verb.new('add','Add a new podcast.', Proc.new {|args| @args[:action] = :add; @args[:arguments] = args}),
|
11
|
+
verb.new('catchup','Mark older episodes as downloaded.', Proc.new {|args| @args[:action] = :catchup; @args[:arguments] = args}),
|
12
|
+
verb.new('remove','Remove a feed by supplying an id.', Proc.new {|args| @args[:action] = :remove; @args[:arguments] = args}),
|
13
|
+
verb.new('lscasts','List podcasts.', Proc.new {|args| @args[:action] = :list_podcast; @args[:arguments] = args}),
|
14
|
+
verb.new('lseps','List episodes of a podcast by id.', Proc.new {|args| @args[:action] = :list_episodes; @args[:arguments] = args}),
|
15
|
+
verb.new('fetch','Update then download podcasts', Proc.new {|args| @args[:action] = :fetch; @args[:arguments] = args}),
|
16
|
+
verb.new('download','Download podcasts specified by a list of id or all podcasts', Proc.new {|args| @args[:action] = :download; @args[:arguments] = args}),
|
17
|
+
verb.new('update','Update podcasts specified by a list of id or all podcasts', Proc.new {|args| @args[:action] = :update; @args[:arguments] = args}),
|
18
|
+
verb.new("clean", "Remove old content from the database", Proc.new {|args| @args[:action] = :clean; @args[:arguments] = args})
|
19
|
+
]
|
20
|
+
get_args
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_args
|
24
|
+
@args[:path] = "~/.epodder"
|
25
|
+
cmd = CmdParse::CommandParser.new( true, true )
|
26
|
+
cmd.program_name = "ePodder"
|
27
|
+
cmd.program_version = [0, 0, 2]
|
28
|
+
cmd.options = CmdParse::OptionParserWrapper.new do |opt|
|
29
|
+
opt.separator "Global options:"
|
30
|
+
opt.on("-v", "--verbose", "Be verbose when outputting info") {|t| @args[:verbose] = true }
|
31
|
+
opt.on("-c", "--conf-dir [PATH]", "Set the configuration directory") {|path| @args[:path] = path}
|
32
|
+
opt.on("-l", "--log-path [PATH]", "Set logging to the specified file") {|path| @args[:log_file] = path}
|
33
|
+
end
|
34
|
+
|
35
|
+
cmd.add_command( CmdParse::HelpCommand.new )
|
36
|
+
cmd.add_command( CmdParse::VersionCommand.new)
|
37
|
+
|
38
|
+
@verbs.each do |verb|
|
39
|
+
command = CmdParse::Command.new(verb.name, false, false)
|
40
|
+
command.short_desc = verb.desc
|
41
|
+
command.set_execution_block(&verb.block)
|
42
|
+
cmd.add_command(command)
|
43
|
+
end
|
44
|
+
|
45
|
+
cmd.parse
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(name, value=nil, *args)
|
49
|
+
if @args.has_key? name
|
50
|
+
@args[name]
|
51
|
+
else
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Epodder
|
2
|
+
class Configurator
|
3
|
+
@@default_path = "~/.epodder/"
|
4
|
+
@@db_path = "epodder.db"
|
5
|
+
@@yaml_path = "epodder.yaml"
|
6
|
+
@@default = {
|
7
|
+
:path_to_db => "epodder.db",
|
8
|
+
:path_to_download => "~/podcasts"
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize args
|
12
|
+
@args = args
|
13
|
+
load_working_dir! @args.path
|
14
|
+
load_config!
|
15
|
+
start_logging
|
16
|
+
load_download_dir!
|
17
|
+
load_database!
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def load_config!
|
22
|
+
#Check to see if we have a config file or if we need to create it
|
23
|
+
if !File.exists? @@yaml_path
|
24
|
+
begin
|
25
|
+
File.open(@@yaml_path, "w") do |io|
|
26
|
+
YAML.dump(@@default, io)
|
27
|
+
end
|
28
|
+
rescue SystemCallError, NameError => error
|
29
|
+
puts "Could not load #{@@yaml_path}: #{error}"
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
end
|
33
|
+
@conf = YAML.load_file(@@yaml_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def load_download_dir!
|
39
|
+
Dir.mkdir File.expand_path(@conf[:path_to_download]) unless Dir.exists?(File.expand_path(@conf[:path_to_download]))
|
40
|
+
File.symlink(File.expand_path(@conf[:path_to_download]), "download") unless File.exists? "download"
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_database!
|
44
|
+
#Datamapper magic goes here
|
45
|
+
DataMapper.setup(:default, "sqlite://#{Dir.pwd}/#{@conf[:path_to_db]}")
|
46
|
+
DataMapper.finalize
|
47
|
+
DataMapper.auto_upgrade!
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_working_dir! path
|
51
|
+
@path = File.expand_path(path.nil? ? @@default_path : path)
|
52
|
+
Dir.mkdir @path unless Dir.exists? @path
|
53
|
+
Dir.chdir @path unless file_error
|
54
|
+
end
|
55
|
+
|
56
|
+
def file_error
|
57
|
+
if !File.directory? @path
|
58
|
+
puts "#{@path} is not a directory"
|
59
|
+
exit
|
60
|
+
elsif !File.readable? @path
|
61
|
+
puts "Can not read #{@path}"
|
62
|
+
exit
|
63
|
+
elsif !File.writable? @path
|
64
|
+
puts "Can not write to #{@path}"
|
65
|
+
exit
|
66
|
+
end
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def start_logging
|
71
|
+
logger = Yell.new :name => 'log' do |l|
|
72
|
+
if @args.log_file.nil?
|
73
|
+
if @args.verbose
|
74
|
+
l.adapter :stdout, "epodder.log", :level => Yell.level(:info)
|
75
|
+
else
|
76
|
+
l.adapter :stdout, "epodder.log", :level => Yell.level(:error)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
if @args.verbose
|
80
|
+
l.adapter :datefile, "epodder.log", :level => Yell.level(:info)
|
81
|
+
else
|
82
|
+
l.adapter :datefile, "epodder.log", :level => Yell.level(:error)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
logger.info "Loaded Logger"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'data_mapper'
|
2
|
+
module Epodder
|
3
|
+
class Episode
|
4
|
+
include DataMapper::Resource
|
5
|
+
|
6
|
+
property :id, Serial
|
7
|
+
property :title, String, :length=> 500
|
8
|
+
property :url, URI
|
9
|
+
property :downloaded, Boolean
|
10
|
+
property :pub_date, DateTime
|
11
|
+
|
12
|
+
belongs_to :podcast
|
13
|
+
|
14
|
+
def self.lookup episode
|
15
|
+
return if episode.enclosure.nil?
|
16
|
+
@episode = Episode.first_or_create(
|
17
|
+
:title => episode.title,
|
18
|
+
:url => episode.enclosure.url,
|
19
|
+
:pub_date => episode.pubdate,
|
20
|
+
:downloaded => false
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def mark_as_downloaded
|
25
|
+
self.downloaded = true
|
26
|
+
success = self.save
|
27
|
+
if !success
|
28
|
+
self.errors.each do |e|
|
29
|
+
puts e
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/eclass.rb
ADDED
data/lib/epodder.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Epodder
|
2
|
+
require "rubygems"
|
3
|
+
require "bundler/setup"
|
4
|
+
require 'require_all'
|
5
|
+
require 'data_mapper'
|
6
|
+
require 'cmdparse'
|
7
|
+
require 'yell'
|
8
|
+
|
9
|
+
require_all File.dirname(File.dirname(__FILE__)) << '/lib'
|
10
|
+
|
11
|
+
@@verbose = false
|
12
|
+
|
13
|
+
def verbose?
|
14
|
+
@@verbose
|
15
|
+
end
|
16
|
+
|
17
|
+
def verbose= state
|
18
|
+
@@verbose = state
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.do_verb verb, args
|
22
|
+
c = Epodder.const_get(verb.to_s.capitalize)
|
23
|
+
verb_object = c.send :new
|
24
|
+
verb_object.send verb, args
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.run
|
28
|
+
args = Arguments.new
|
29
|
+
Configurator.new(args)
|
30
|
+
do_verb args.action, args.arguments
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/verb/add.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Epodder
|
2
|
+
class Add < Verb
|
3
|
+
def initialize
|
4
|
+
@mechanize = Mechanize.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def add args
|
8
|
+
args.each do |url|
|
9
|
+
lookup_podcast url
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def lookup_podcast url
|
14
|
+
@mechanize.get(url) do |feed|
|
15
|
+
save_podcast feed, url
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_podcast feed, url
|
20
|
+
cast = FeedMe.parse feed.body
|
21
|
+
cast.emulate_atom!
|
22
|
+
podcast = Podcast.first_or_create(
|
23
|
+
:title => cast.title,
|
24
|
+
:uri => url
|
25
|
+
)
|
26
|
+
podcast.save
|
27
|
+
|
28
|
+
podcast.errors.each do |error|
|
29
|
+
@log.error error
|
30
|
+
end
|
31
|
+
puts "#{podcast.id} - #{podcast.title}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/verb/catchup.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Epodder
|
2
|
+
class Catchup < Verb
|
3
|
+
def initialize
|
4
|
+
end
|
5
|
+
|
6
|
+
def catchup args
|
7
|
+
if args.empty?
|
8
|
+
podcasts = Podcast.all
|
9
|
+
else
|
10
|
+
podcasts = args.map {|id| Podcast.get(id)}
|
11
|
+
end
|
12
|
+
podcasts.each do |podcast|
|
13
|
+
Episode.all(:downloaded => false, :podcast => podcast).update(:downloaded => true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/lib/verb/clean.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Epodder
|
2
|
+
class Clean < Verb
|
3
|
+
def initialize
|
4
|
+
@mechanize = @mechanize = Mechanize.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def clean args
|
8
|
+
if args.empty?
|
9
|
+
podcasts = Podcast.all
|
10
|
+
else
|
11
|
+
podcasts = args.map{|podcast| Podcast.get(podcast)}
|
12
|
+
end
|
13
|
+
podcasts.each do |podcast|
|
14
|
+
count = 0
|
15
|
+
known_episodes = Episode.all(:downloaded => true, :podcast => podcast)
|
16
|
+
feed_episodes = open_podcast podcast, DateTime.now
|
17
|
+
known_episodes = known_episodes.map{|episode| episode.url.to_s}
|
18
|
+
feed_episodes = feed_episodes.map{|episode|@episode = episode; episode.enclosure.url}
|
19
|
+
episodes = (known_episodes - feed_episodes)
|
20
|
+
puts "know episodes was #{known_episodes.length - feed_episodes.length} longer than feed episodes"
|
21
|
+
episodes.map{|episode| Episode.all(:url => episode)}.each do |episode|
|
22
|
+
count += 1
|
23
|
+
win = episode.destroy
|
24
|
+
if !win
|
25
|
+
episode.errors.each {|error| puts error}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
puts "#{podcast.title} -- cleaned #{count} episodes"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def open_podcast podcast, max_pub
|
33
|
+
begin
|
34
|
+
@mechanize.get(podcast.uri) do |feed|
|
35
|
+
return parse_feed feed, max_pub
|
36
|
+
end
|
37
|
+
rescue Exception => e
|
38
|
+
puts e
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_feed feed, max_pub
|
43
|
+
podcast = FeedMe.parse feed.body
|
44
|
+
podcast.emulate_atom!
|
45
|
+
temp = podcast.items.select {|item| !item.enclosure.nil?}
|
46
|
+
temp.select {|item| !item.enclosure.empty?}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
#require 'mechanize/progressbar'
|
3
|
+
#require 'progress'
|
4
|
+
module Epodder
|
5
|
+
class Download < Verb
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@mechanize = Mechanize.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def download args
|
12
|
+
if args.empty?
|
13
|
+
podcasts = Podcast.all
|
14
|
+
else
|
15
|
+
podcasts = args.map {|id| Podcast.get(id)}
|
16
|
+
end
|
17
|
+
look_for_episodes podcasts
|
18
|
+
end
|
19
|
+
|
20
|
+
def look_for_episodes podcasts
|
21
|
+
podcasts.each do |podcast|
|
22
|
+
episodes = Episode.all(:downloaded => false, :podcast => podcast)
|
23
|
+
episodes.select{|ep| !ep.nil?}.each do |episode|
|
24
|
+
puts episode.podcast.title
|
25
|
+
title = (episode.podcast.title).strip
|
26
|
+
Dir.mkdir "download/#{title}" unless Dir.exists? "download/#{title}"
|
27
|
+
puts "Downloading #{title} - #{episode.title || episode.id}"
|
28
|
+
download_episode episode
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def download_episode episode
|
34
|
+
begin
|
35
|
+
@mechanize.get(episode.url).save_as("download/#{episode.podcast.title.strip}/#{episode.url.to_s.match('((?!\/).)*$')}")
|
36
|
+
episode.mark_as_downloaded
|
37
|
+
rescue Exception => e
|
38
|
+
puts e
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
data/lib/verb/fetch.rb
ADDED
data/lib/verb/list.rb
ADDED
data/lib/verb/remove.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
module Epodder
|
3
|
+
class Remove < Verb
|
4
|
+
def initialize
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove *args
|
9
|
+
args.map{|id| Podcast.get(id)}.each do |podcast|
|
10
|
+
input = ask("Remove #{podcast.id} : #{podcast.title}? Type \"YES\" to remove")
|
11
|
+
if input == "YES"
|
12
|
+
podcast.destroy
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/verb/update.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'feedme'
|
3
|
+
require 'mechanize'
|
4
|
+
#require 'progress'
|
5
|
+
module Epodder
|
6
|
+
class Update < Verb
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@mechanize = Mechanize.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def update args
|
14
|
+
if args.empty?
|
15
|
+
podcasts = Podcast.all
|
16
|
+
else
|
17
|
+
podcasts = args.map {|id| Podcast.get(id)}
|
18
|
+
end
|
19
|
+
check_for_new_episodes podcasts if podcasts.any?
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_for_new_episodes podcasts
|
23
|
+
podcasts.each do |podcast|
|
24
|
+
@podcast = podcast
|
25
|
+
@podcast_urls = @podcast.episodes.map {|episode| episode.url}
|
26
|
+
max_pub = get_max_pubdate podcast
|
27
|
+
open_podcast podcast, max_pub
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_max_pubdate podcast
|
32
|
+
Episode.max(:pub_date, :conditions => { :podcast => podcast}) || Time.at(0).to_date
|
33
|
+
end
|
34
|
+
|
35
|
+
def open_podcast podcast, max_pub
|
36
|
+
begin
|
37
|
+
@mechanize.get(podcast.uri) do |feed|
|
38
|
+
@log.info "Maximum pubdate for #{podcast.title} - #{podcast.id} is #{max_pub}"
|
39
|
+
parse_feed feed, max_pub
|
40
|
+
end
|
41
|
+
rescue Exception => e
|
42
|
+
puts e
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_feed feed, max_pub
|
47
|
+
podcast = FeedMe.parse feed.body
|
48
|
+
podcast.emulate_atom!
|
49
|
+
@count = 0
|
50
|
+
podcast.items.each do |item|
|
51
|
+
add_eligable_episodes item, max_pub
|
52
|
+
end
|
53
|
+
puts "#{@podcast.title} has #{@count} new episodes" if @count > 0
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_eligable_episodes item, max_pub
|
57
|
+
if !item.enclosure.nil? && item.pubdate.to_date > max_pub.to_date
|
58
|
+
begin
|
59
|
+
@count += 1
|
60
|
+
ep = Episode.first_or_create(
|
61
|
+
:title => item.title,
|
62
|
+
:url => item.enclosure.url,
|
63
|
+
:pub_date => item.pubdate.to_date,
|
64
|
+
:downloaded => false,
|
65
|
+
:podcast => @podcast
|
66
|
+
)
|
67
|
+
ep.errors.each do |error|
|
68
|
+
@log.error error
|
69
|
+
end
|
70
|
+
|
71
|
+
rescue Exception => e
|
72
|
+
@log.error e
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
data/lib/verb/verb.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Epodder
|
2
|
+
class Verb < Eclass
|
3
|
+
def verb_struct
|
4
|
+
super
|
5
|
+
Struct.new(:name,:description,:block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_command (cmd, args)
|
9
|
+
command = CmdParse::Command.new(@verb.name,false,false)
|
10
|
+
command.short_desc = @verb.description
|
11
|
+
command.set_execution_block(&@verb.block)
|
12
|
+
cmd.add_command(command)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: epodder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Bergstrom
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-25 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: bandwidthoracle@gmail.com
|
16
|
+
executables:
|
17
|
+
- epodder
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/arguments.rb
|
22
|
+
- lib/configuration/configuration.rb
|
23
|
+
- lib/configuration/configurator.rb
|
24
|
+
- lib/database/episode.rb
|
25
|
+
- lib/database/podcast.rb
|
26
|
+
- lib/eclass.rb
|
27
|
+
- lib/epodder.rb
|
28
|
+
- lib/verb/add.rb
|
29
|
+
- lib/verb/catchup.rb
|
30
|
+
- lib/verb/clean.rb
|
31
|
+
- lib/verb/download.rb
|
32
|
+
- lib/verb/fetch.rb
|
33
|
+
- lib/verb/list.rb
|
34
|
+
- lib/verb/remove.rb
|
35
|
+
- lib/verb/update.rb
|
36
|
+
- lib/verb/verb.rb
|
37
|
+
- bin/epodder
|
38
|
+
homepage: http://github.com/scribe/epodder
|
39
|
+
licenses: []
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.8.25
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: Ruby re-do of hpodder
|
62
|
+
test_files: []
|