dropcaster 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -1
- data/README.md +18 -2
- data/TODO +3 -5
- data/VERSION +1 -1
- data/bin/dropcaster +19 -19
- data/doc/sample-channel.yml +5 -3
- data/dropcaster.gemspec +12 -3
- data/lib/dropcaster/channel.rb +75 -12
- data/lib/dropcaster/channel_file_locator.rb +12 -3
- data/lib/dropcaster/errors.rb +1 -1
- data/lib/dropcaster/item.rb +14 -10
- data/lib/dropcaster/log_formatter.rb +9 -0
- data/lib/dropcaster.rb +12 -0
- data/templates/channel.html.erb +43 -0
- data/templates/channel.rss.erb +64 -0
- data/test/fixtures/channel.yml +14 -12
- data/test/fixtures/test_template.json.erb +40 -0
- data/test/unit/test_app.rb +24 -9
- data/test/unit/test_channel.rb +43 -2
- data/test/unit/test_channel_locator.rb +5 -3
- data/test/unit/test_channel_xml.rb +9 -7
- data/test/unit/test_item.rb +1 -1
- metadata +60 -27
- data/templates/iTunes.rss.erb +0 -57
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -7,9 +7,10 @@ GEM
|
|
7
7
|
bundler (~> 1.0)
|
8
8
|
git (>= 1.2.5)
|
9
9
|
rake
|
10
|
+
json (1.5.3)
|
10
11
|
libxml-ruby (2.2.1)
|
11
12
|
libxml-ruby (2.2.1-x86-mingw32)
|
12
|
-
rake (0.
|
13
|
+
rake (0.8.7)
|
13
14
|
rdoc (3.9.3)
|
14
15
|
ruby-mp3info (0.6.15)
|
15
16
|
|
@@ -21,6 +22,8 @@ DEPENDENCIES
|
|
21
22
|
activesupport
|
22
23
|
bundler (~> 1.0.0)
|
23
24
|
jeweler (~> 1.6.4)
|
25
|
+
json
|
24
26
|
libxml-ruby
|
27
|
+
rake (= 0.8.7)
|
25
28
|
rdoc
|
26
29
|
ruby-mp3info
|
data/README.md
CHANGED
@@ -110,7 +110,7 @@ Advanced features
|
|
110
110
|
=================
|
111
111
|
Overriding defaults
|
112
112
|
-------------------
|
113
|
-
Dropcaster is opinionated software. That means, it makes a number of assumptions about names, files, and directory
|
113
|
+
Dropcaster is opinionated software. That means, it makes a number of assumptions about names, files, and directory structures. Dropcaster will be most simple to use if these assumptions and opinions apply to your way of using the program.
|
114
114
|
|
115
115
|
However, it is still possible to override Dropcaster's behavior in many ways. You can, for instance, host your episode files on a different URL than the channel. Instead of writing title, subtitle, etc. to a channel.yml, you may also spedify them on the command line.
|
116
116
|
|
@@ -118,6 +118,10 @@ In order to find out about all the options, simply run
|
|
118
118
|
|
119
119
|
$ dropcaster --help
|
120
120
|
|
121
|
+
Using custom channel templates
|
122
|
+
------------------------------
|
123
|
+
Dropcaster generates a feed that is suitable for most podcast clients, especially iTunes. It is also possible to customize the channel by supplying an alternative channel template on the command line. Start your own template by copying the default template, or look at the test directory of the dropcaster gem. You can get there by running `gem open dropcaster`.
|
124
|
+
|
121
125
|
Sidecar files
|
122
126
|
-------------
|
123
127
|
You may override the meta data for any episode by providing a YAML file with the same name as the mp3 file, but with an extension of yml or yaml (ususally refered to as [sidecar file](http://en.wikipedia.org/wiki/Sidecar_file)). Any attributes specified in this file override the ID tags in the mp3 file.
|
@@ -134,10 +138,22 @@ The whole concept of Dropcaster works perfectly fine without Dropbox. Just run t
|
|
134
138
|
|
135
139
|
Episode Identifier (uuid)
|
136
140
|
-------------------------
|
137
|
-
Dropcaster uses a rather simple approach to uniquely identify the episodes. It simply generates a SHA1 hash of the mp3 file. If it changes, for whatever reason (even if only a tag was
|
141
|
+
Dropcaster uses a rather simple approach to uniquely identify the episodes. It simply generates a SHA1 hash of the mp3 file. If it changes, for whatever reason (even if only a tag was changed), the episode will get a new UUID, and any podcatcher will fetch the episode again (which is what you want, in most cases).
|
138
142
|
|
139
143
|
Modifying the sidecar file does not change the UUID, because it only affects the feed and not the episode itself.
|
140
144
|
|
145
|
+
I Don't Like the Output Format that Dropcaster produces
|
146
|
+
-------------------------------------------------------
|
147
|
+
Dropcaster uses an ERB template to generate the XML feed. The template was written so that it is easy to understand, but not necessarily in a way that would make the output rather nice-looking. That should not be an issue, as long as the XML is correct.
|
148
|
+
|
149
|
+
It you prefer a more aesthetically pleasing output, just pipe the output of Dropcaster through `xmllint`, which is part of [libxml](http://xmlsoft.org/), which in turn is one of the prerequisites of the Dropcaster gem, and, as such, installed with Dropcaster):
|
150
|
+
|
151
|
+
dropcaster | xmllint --format -
|
152
|
+
|
153
|
+
For writing the output to a file, just redirect the ouput of the above command:
|
154
|
+
|
155
|
+
dropcaster | xmllint --format - > index.rss
|
156
|
+
|
141
157
|
Contributing to Dropcaster
|
142
158
|
==========================
|
143
159
|
Dropcaster is hosted at [Github](http://github.com/nerab/dropcaster):
|
data/TODO
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
* Handle channel URLs not ending with a slash (may be required for hosts not discovering an index.html, like Dropbox)
|
2
|
-
*
|
3
|
-
*
|
4
|
-
* Support a tree of iTunes categories: <itunes:category text="Technology"><itunes:category text="Gadgets"/></itunes:category>
|
5
|
-
* If an index.html.erb is present, generate an index.html from it
|
6
|
-
** In addition to that, if an item.html.erb is present, generate individual html for each item from it and link to it from the index.html
|
2
|
+
* Support subtitle and keywords in channel and items
|
3
|
+
* If the option for the item template is present (e.g. --item-template templates/item.html.erb), generate individual html pages for each item from it and link to it from the index.html. Leave it off by default.
|
7
4
|
* Print warnings when one of the specs from http://www.apple.com/itunes/podcasts/specs.html is violated
|
8
5
|
* Allow muting of the iTunes warnings (see above) using a commandline switch
|
9
6
|
* Implement sidecar files
|
@@ -12,3 +9,4 @@
|
|
12
9
|
* Potential optimization for large numbers of mp3 files: If there is an existing index.rss file, do not re-read the information from those files that are still there and still have the same hash value.
|
13
10
|
* It feels pretty awkward to refer to the ID3v2 frame names all the time. After all, this is an implementation detail. Maybe we can wrap the frames with a nicer name, but still allow access to the underlying mp3info library? Maybe all we need is some aliasing and / or delegation?
|
14
11
|
* Potentially replace the ERB template with Builder::XmlMarkup, see http://builder.rubyforge.org/ (the same that Rails uses)
|
12
|
+
* Expose strip_itunes_private as commandline parameter
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.4
|
data/bin/dropcaster
CHANGED
@@ -18,7 +18,6 @@ Basic Usage:
|
|
18
18
|
dropcaster [DIR]... Prints a podcast feed document for the mp3 files in DIR
|
19
19
|
|
20
20
|
Options:
|
21
|
-
|
22
21
|
HELP
|
23
22
|
|
24
23
|
def usage
|
@@ -29,55 +28,56 @@ require 'optparse'
|
|
29
28
|
require 'dropcaster'
|
30
29
|
|
31
30
|
options = Hash.new
|
32
|
-
options[:verbose] = false
|
33
31
|
options[:auto_detect_channel_file] = true
|
34
32
|
|
35
33
|
opts = OptionParser.new do |opts|
|
36
34
|
opts.banner = help
|
37
35
|
|
38
36
|
opts.on("--verbose", "Verbose mode - displays additional diagnostic information") do |file|
|
39
|
-
|
37
|
+
Dropcaster.logger = Logger.new(STDERR)
|
38
|
+
Dropcaster.logger.formatter = Dropcaster::LogFormatter.new
|
39
|
+
Dropcaster.logger.level = Logger::INFO
|
40
40
|
end
|
41
41
|
|
42
42
|
opts.on("--channel FILE", "Read the channel definition from FILE instead of channel.yml in the current directory.") do |file|
|
43
43
|
begin
|
44
|
-
|
44
|
+
Dropcaster.logger.info "Reading channel definition from #{file}"
|
45
45
|
options = YAML.load_file(file).merge(options)
|
46
46
|
options[:auto_detect_channel_file] = false
|
47
47
|
rescue
|
48
|
-
|
49
|
-
|
48
|
+
Dropcaster.logger.error "Could not load channel definition. #{$!.message}"
|
49
|
+
Dropcaster.logger.info $!.backtrace
|
50
50
|
exit(1)
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
54
|
opts.on("--title STRING", "Use STRING as the channel's title. Overrides settings read from channel definition file.") do |title|
|
55
|
-
|
55
|
+
Dropcaster.logger.info "Setting channel title to '#{title}' via command line"
|
56
56
|
options[:title] = title
|
57
57
|
end
|
58
58
|
|
59
59
|
opts.on("--url URL", "Use URL as the channel's url. Overrides settings read from channel definition file.") do |url|
|
60
|
-
|
60
|
+
Dropcaster.logger.info "Setting channel URL to '#{url}' via command line"
|
61
61
|
options[:url] = url
|
62
62
|
end
|
63
63
|
|
64
64
|
opts.on("--description STRING", "Use STRING as the channel's description. Overrides settings read from channel definition file.") do |description|
|
65
|
-
|
65
|
+
Dropcaster.logger.info "Setting channel description to '#{description}' via command line"
|
66
66
|
options[:description] = description
|
67
67
|
end
|
68
68
|
|
69
69
|
opts.on("--enclosures URL", "Use URL as the base URL for the channel's enclosures. Overrides settings read from channel definition file.") do |enclosures_url|
|
70
|
-
|
70
|
+
Dropcaster.logger.info "Setting enclosures base URL to '#{enclosures_url}' via command line"
|
71
71
|
options[:enclosures_url] = enclosures_url
|
72
72
|
end
|
73
73
|
|
74
74
|
opts.on("--image URL", "Use URL as the channel's image URL. Overrides settings read from channel definition file.") do |image_url|
|
75
|
-
|
75
|
+
Dropcaster.logger.info "Setting image URL to '#{image_url}' via command line"
|
76
76
|
options[:image_url] = image_url
|
77
77
|
end
|
78
78
|
|
79
79
|
opts.on("--channel-template FILE", "Use FILE as template for generating the channel feed. Overrides the default that comes with Dropcaster.") do |file|
|
80
|
-
|
80
|
+
Dropcaster.logger.info "Using'#{file}' as channel template file"
|
81
81
|
options[:channel_template] = file
|
82
82
|
end
|
83
83
|
|
@@ -95,23 +95,23 @@ if options[:auto_detect_channel_file]
|
|
95
95
|
channel_file = Dropcaster::ChannelFileLocator.locate(sources)
|
96
96
|
|
97
97
|
if File.exists?(channel_file)
|
98
|
-
|
98
|
+
Dropcaster.logger.info "Auto-detected channel file at #{channel_file}"
|
99
99
|
options_from_yaml = YAML.load_file(channel_file)
|
100
100
|
options = options_from_yaml.merge(options)
|
101
101
|
else
|
102
|
-
|
103
|
-
|
102
|
+
Dropcaster.logger.error "No channel file found at #{channel_file})"
|
103
|
+
Dropcaster.logger.info usage
|
104
104
|
exit(1) # No way to continue without a channel definition
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
-
|
108
|
+
Dropcaster.logger.info "Generating the channel with these options: #{options.inspect}"
|
109
109
|
|
110
110
|
begin
|
111
111
|
puts Dropcaster::Channel.new(sources, options).to_rss
|
112
112
|
rescue
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
Dropcaster.logger.error $!.message
|
114
|
+
Dropcaster.logger.debug $!.backtrace
|
115
|
+
Dropcaster.logger.info usage
|
116
116
|
exit(1)
|
117
117
|
end
|
data/doc/sample-channel.yml
CHANGED
@@ -69,11 +69,13 @@
|
|
69
69
|
# Examples:
|
70
70
|
# :categories: 'Technology'
|
71
71
|
# :categories: ['Technology', 'Gadgets']
|
72
|
-
# :categories: ['TV &
|
73
|
-
|
72
|
+
# :categories: ['TV & Film', ['Technology', 'Gadgets']]
|
73
|
+
#
|
74
|
+
# Don't HTML-escape the categories, this will be taken care of by Dropcaster
|
75
|
+
#
|
74
76
|
:categories: ['Technology', 'Gadgets']
|
75
77
|
|
76
78
|
#
|
77
79
|
# Yes, No or Clean, see http://www.apple.com/itunes/podcasts/specs.html#explicit
|
78
80
|
#
|
79
|
-
:explicit:
|
81
|
+
:explicit: No
|
data/dropcaster.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{dropcaster}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.4"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["nerab"]
|
12
|
-
s.date = %q{2011-10-
|
12
|
+
s.date = %q{2011-10-24}
|
13
13
|
s.description = %q{Dropcaster is a podcast feed generator for the command line. It is most simple to use with Dropbox, but works equally well with any other hoster.}
|
14
14
|
s.email = %q{nerab@gmx.at}
|
15
15
|
s.executables = ["lstags", "dropcaster", "dropcaster", "lstags"]
|
@@ -41,10 +41,13 @@ Gem::Specification.new do |s|
|
|
41
41
|
"lib/dropcaster/errors.rb",
|
42
42
|
"lib/dropcaster/hashkeys.rb",
|
43
43
|
"lib/dropcaster/item.rb",
|
44
|
-
"
|
44
|
+
"lib/dropcaster/log_formatter.rb",
|
45
|
+
"templates/channel.html.erb",
|
46
|
+
"templates/channel.rss.erb",
|
45
47
|
"test/extensions/windows.rb",
|
46
48
|
"test/fixtures/channel.yml",
|
47
49
|
"test/fixtures/iTunes.mp3",
|
50
|
+
"test/fixtures/test_template.json.erb",
|
48
51
|
"test/helper.rb",
|
49
52
|
"test/unit/test_app.rb",
|
50
53
|
"test/unit/test_channel.rb",
|
@@ -64,25 +67,31 @@ Gem::Specification.new do |s|
|
|
64
67
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
65
68
|
s.add_runtime_dependency(%q<ruby-mp3info>, [">= 0"])
|
66
69
|
s.add_runtime_dependency(%q<activesupport>, [">= 0"])
|
70
|
+
s.add_development_dependency(%q<rake>, ["= 0.8.7"])
|
67
71
|
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
68
72
|
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
69
73
|
s.add_development_dependency(%q<libxml-ruby>, [">= 0"])
|
70
74
|
s.add_development_dependency(%q<rdoc>, [">= 0"])
|
75
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
71
76
|
else
|
72
77
|
s.add_dependency(%q<ruby-mp3info>, [">= 0"])
|
73
78
|
s.add_dependency(%q<activesupport>, [">= 0"])
|
79
|
+
s.add_dependency(%q<rake>, ["= 0.8.7"])
|
74
80
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
75
81
|
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
76
82
|
s.add_dependency(%q<libxml-ruby>, [">= 0"])
|
77
83
|
s.add_dependency(%q<rdoc>, [">= 0"])
|
84
|
+
s.add_dependency(%q<json>, [">= 0"])
|
78
85
|
end
|
79
86
|
else
|
80
87
|
s.add_dependency(%q<ruby-mp3info>, [">= 0"])
|
81
88
|
s.add_dependency(%q<activesupport>, [">= 0"])
|
89
|
+
s.add_dependency(%q<rake>, ["= 0.8.7"])
|
82
90
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
83
91
|
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
84
92
|
s.add_dependency(%q<libxml-ruby>, [">= 0"])
|
85
93
|
s.add_dependency(%q<rdoc>, [">= 0"])
|
94
|
+
s.add_dependency(%q<json>, [">= 0"])
|
86
95
|
end
|
87
96
|
end
|
88
97
|
|
data/lib/dropcaster/channel.rb
CHANGED
@@ -6,28 +6,31 @@ module Dropcaster
|
|
6
6
|
# Represents a podcast feed in the RSS 2.0 format
|
7
7
|
#
|
8
8
|
class Channel < DelegateClass(Hash)
|
9
|
+
include ERB::Util # for h() in the ERB template
|
9
10
|
include HashKeys
|
10
11
|
|
12
|
+
STORAGE_UNITS = %w(Byte KB MB GB TB)
|
13
|
+
|
11
14
|
# Instantiate a new Channel object. +sources+ must be present and can be a String or Array
|
12
15
|
# of Strings, pointing to a one or more directories or MP3 files.
|
13
16
|
#
|
14
|
-
# +
|
17
|
+
# +attributes+ is a hash with all attributes for the channel. The following attributes are
|
15
18
|
# mandatory when a new channel is created:
|
16
19
|
#
|
17
20
|
# * <tt>:title</tt> - Title (name) of the podcast
|
18
21
|
# * <tt>:url</tt> - URL to the podcast
|
19
22
|
# * <tt>:description</tt> - Short description of the podcast (a few words)
|
20
23
|
#
|
21
|
-
def initialize(sources,
|
24
|
+
def initialize(sources, attributes)
|
22
25
|
super(Hash.new)
|
23
26
|
|
24
|
-
# Assert mandatory
|
27
|
+
# Assert mandatory attributes
|
25
28
|
[:title, :url, :description].each{|attr|
|
26
|
-
raise MissingAttributeError.new(attr) if
|
29
|
+
raise MissingAttributeError.new(attr) if attributes[attr].blank?
|
27
30
|
}
|
28
31
|
|
29
|
-
self.merge!(
|
30
|
-
self.
|
32
|
+
self.merge!(attributes)
|
33
|
+
self.explicit = yes_no_or_input(attributes[:explicit])
|
31
34
|
@source_files = Array.new
|
32
35
|
|
33
36
|
if (sources.respond_to?(:each)) # array
|
@@ -40,9 +43,18 @@ module Dropcaster
|
|
40
43
|
end
|
41
44
|
|
42
45
|
# Prepend the image URL with the channel's base to make an absolute URL
|
43
|
-
|
46
|
+
unless self.image_url.blank? || self.image_url =~ /^https?:/
|
47
|
+
Dropcaster.logger.info("Channel image URL '#{self.image_url}' is relative, so we prepend it with the channel URL '#{self.url}'")
|
48
|
+
self.image_url = URI.join(self.url, self.image_url).to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
# If enclosures_url is not given, take the channel URL as a base.
|
52
|
+
if self.enclosures_url.blank?
|
53
|
+
Dropcaster.logger.info("No enclosure URL given, using the channel's enclosure URL")
|
54
|
+
self.enclosures_url = self.url
|
55
|
+
end
|
44
56
|
|
45
|
-
channel_template = self.channel_template || File.join(File.dirname(__FILE__), '..', '..', 'templates', '
|
57
|
+
channel_template = self.channel_template || File.join(File.dirname(__FILE__), '..', '..', 'templates', 'channel.rss.erb')
|
46
58
|
|
47
59
|
begin
|
48
60
|
@erb_template = ERB.new(File.new(channel_template), 0, "%<>")
|
@@ -68,13 +80,20 @@ module Dropcaster
|
|
68
80
|
@source_files.each{|src|
|
69
81
|
item = Item.new(src)
|
70
82
|
|
83
|
+
Dropcaster.logger.debug("Adding new item from file #{src}")
|
84
|
+
|
71
85
|
# set author and image_url from channel if empty
|
72
|
-
|
73
|
-
|
86
|
+
if item.artist.blank?
|
87
|
+
Dropcaster.logger.info("#{src} has no artist, using the channel's author")
|
88
|
+
item.tag.artist = self.author
|
89
|
+
end
|
90
|
+
|
91
|
+
if item.image_url.blank?
|
92
|
+
Dropcaster.logger.info("#{src} has no image URL set, using the channel's image URL")
|
93
|
+
item.image_url = self.image_url
|
94
|
+
end
|
74
95
|
|
75
96
|
# construct absolute URL, based on the channel's enclosures_url attribute
|
76
|
-
# If enclosures_url is not given, take the channel URL as a base.
|
77
|
-
self.enclosures_url = self.url if self.enclosures_url.blank?
|
78
97
|
self.enclosures_url << '/' unless self.enclosures_url =~ /\/$/
|
79
98
|
item.url = URI.join(URI.escape(self.enclosures_url), URI.escape(item.file_name))
|
80
99
|
|
@@ -84,6 +103,37 @@ module Dropcaster
|
|
84
103
|
all_items.sort{|x, y| y.pub_date <=> x.pub_date}
|
85
104
|
end
|
86
105
|
|
106
|
+
# from http://stackoverflow.com/questions/4136248
|
107
|
+
def humanize_time(secs)
|
108
|
+
[[60, :s], [60, :m], [24, :h], [1000, :d]].map{ |count, name|
|
109
|
+
if secs > 0
|
110
|
+
secs, n = secs.divmod(count)
|
111
|
+
"#{n.to_i}#{name}"
|
112
|
+
end
|
113
|
+
}.compact.reverse.join(' ')
|
114
|
+
end
|
115
|
+
|
116
|
+
# Fixed version of https://gist.github.com/260184
|
117
|
+
def humanize_size(number)
|
118
|
+
return nil if number.nil?
|
119
|
+
|
120
|
+
storage_units_format = '%n %u'
|
121
|
+
|
122
|
+
if number.to_i < 1024
|
123
|
+
unit = number > 1 ? 'Bytes' : 'Byte'
|
124
|
+
return storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
|
125
|
+
else
|
126
|
+
max_exp = STORAGE_UNITS.size - 1
|
127
|
+
number = Float(number)
|
128
|
+
exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
|
129
|
+
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
|
130
|
+
number /= 1024 ** exponent
|
131
|
+
|
132
|
+
unit = STORAGE_UNITS[exponent]
|
133
|
+
return storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
87
137
|
private
|
88
138
|
def add_files(src)
|
89
139
|
if File.directory?(src)
|
@@ -92,5 +142,18 @@ module Dropcaster
|
|
92
142
|
@source_files << src
|
93
143
|
end
|
94
144
|
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Deal with Ruby's autoboxing of Yes, No, true, etc values in YAML
|
148
|
+
#
|
149
|
+
def yes_no_or_input(flag)
|
150
|
+
case flag
|
151
|
+
when nil : nil
|
152
|
+
when true : 'Yes'
|
153
|
+
when false : 'No'
|
154
|
+
else
|
155
|
+
flag
|
156
|
+
end
|
157
|
+
end
|
95
158
|
end
|
96
159
|
end
|
@@ -27,7 +27,7 @@ module Dropcaster
|
|
27
27
|
|
28
28
|
if sources.respond_to?(:at)
|
29
29
|
# More than one source given. Check that they are all in the same directory.
|
30
|
-
distinct_dirs = sources.collect{|source|
|
30
|
+
distinct_dirs = sources.collect{|source| dir_or_self(source)}.uniq
|
31
31
|
|
32
32
|
if 1 == distinct_dirs.size
|
33
33
|
# If all are the in same directory, use that as source directory where channel.yml is expected.
|
@@ -38,11 +38,20 @@ module Dropcaster
|
|
38
38
|
end
|
39
39
|
else
|
40
40
|
# If a single file or directory is given, use that as source directory where channel.yml is expected.
|
41
|
-
channel_source_dir =
|
41
|
+
channel_source_dir = dir_or_self(sources)
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
File.join(channel_source_dir, CHANNEL_YML)
|
45
45
|
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def dir_or_self(source)
|
49
|
+
if File.directory?(source)
|
50
|
+
source
|
51
|
+
else
|
52
|
+
File.dirname(source)
|
53
|
+
end
|
54
|
+
end
|
46
55
|
end
|
47
56
|
end
|
48
57
|
end
|
data/lib/dropcaster/errors.rb
CHANGED
@@ -7,7 +7,7 @@ module Dropcaster
|
|
7
7
|
|
8
8
|
class MissingAttributeError < ConfigurationError
|
9
9
|
def initialize(missingAttribute)
|
10
|
-
super("#{missingAttribute} is a mandatory attribute, but it is missing.")
|
10
|
+
super("#{missingAttribute} is a mandatory channel attribute, but it is missing.")
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
data/lib/dropcaster/item.rb
CHANGED
@@ -17,23 +17,27 @@ module Dropcaster
|
|
17
17
|
|
18
18
|
self[:file_size] = File.new(self.file_name).stat.size
|
19
19
|
self[:uuid] = Digest::SHA1.hexdigest(File.read(self.file_name))
|
20
|
-
|
21
|
-
|
22
|
-
self[:pub_date] = DateTime.parse(File.new(self.file_name).mtime.to_s)
|
23
|
-
else
|
20
|
+
|
21
|
+
unless self.tag2.TDR.blank?
|
24
22
|
self[:pub_date] = DateTime.parse(self.tag2.TDR)
|
23
|
+
else
|
24
|
+
Dropcaster.logger.info("#{file_path} has no pub date set, using the file's modification time")
|
25
|
+
self[:pub_date] = DateTime.parse(File.new(self.file_name).mtime.to_s)
|
25
26
|
end
|
26
|
-
|
27
|
+
|
27
28
|
# Remove iTunes normalization crap (if configured)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
if options && options[:strip_itunes_private]
|
30
|
+
Dropcaster.logger.info("Removing iTunes' private normalization information from comments")
|
31
|
+
self.tag2.COM.delete_if{|comment|
|
32
|
+
comment =~ /^( [0-9A-F]{8}){10}$/
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
32
36
|
# Convert lyrics frame into a hash, keyed by the three-letter language code
|
33
37
|
if tag2.ULT
|
34
38
|
lyrics_parts = tag2.ULT.split(0.chr)
|
35
39
|
|
36
|
-
if lyrics_parts && 3 == lyrics_parts.size
|
40
|
+
if lyrics_parts && 3 == lyrics_parts.size
|
37
41
|
self.lyrics = Hash.new
|
38
42
|
self.lyrics[lyrics_parts[1]] = lyrics_parts[2]
|
39
43
|
end
|
data/lib/dropcaster.rb
CHANGED
@@ -5,8 +5,12 @@ require 'delegate'
|
|
5
5
|
require 'yaml'
|
6
6
|
require 'active_support/core_ext/date_time/conversions'
|
7
7
|
require 'active_support/core_ext/object/blank'
|
8
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
9
|
+
require 'logger'
|
10
|
+
require 'active_support/core_ext/logger'
|
8
11
|
|
9
12
|
require 'dropcaster/errors'
|
13
|
+
require 'dropcaster/log_formatter'
|
10
14
|
require 'dropcaster/hashkeys'
|
11
15
|
require 'dropcaster/channel'
|
12
16
|
require 'dropcaster/item'
|
@@ -15,4 +19,12 @@ require 'dropcaster/channel_file_locator'
|
|
15
19
|
module Dropcaster
|
16
20
|
VERSION = File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION]))
|
17
21
|
CHANNEL_YML = 'channel.yml'
|
22
|
+
|
23
|
+
mattr_accessor :logger
|
24
|
+
|
25
|
+
unless @@logger
|
26
|
+
@@logger = Logger.new(STDERR)
|
27
|
+
@@logger.level = Logger::WARN
|
28
|
+
@@logger.formatter = LogFormatter.new
|
29
|
+
end
|
18
30
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="<%= h(language) %>">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8"/>
|
5
|
+
<title><%= h(title) %></title>
|
6
|
+
<meta name="generator" content="Dropcaster <%= Dropcaster::VERSION%>"/>
|
7
|
+
<% unless copyright.blank? %>
|
8
|
+
<meta name="copyright" content="<%= h(copyright) %>"/>
|
9
|
+
<% end %>
|
10
|
+
<link rel="canonical" href="<%= h(url) %>"/>
|
11
|
+
</head>
|
12
|
+
|
13
|
+
<body id="home">
|
14
|
+
<h1><%= h(title) %></h1>
|
15
|
+
<% unless subtitle.blank? %>
|
16
|
+
<h2><%= h(subtitle) %></h2>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<% unless image_url.blank? %>
|
20
|
+
<img src="<%= image_url %>"/>
|
21
|
+
<% end %>
|
22
|
+
<p><%= h(description) %></p>
|
23
|
+
|
24
|
+
<h1>Episodes</h1>
|
25
|
+
<% items.each{|item| %>
|
26
|
+
<div class="item" id="<%= h(item.uuid) %>">
|
27
|
+
<h1><%= item.tag.title || item.tag2.TIT2%></h1>
|
28
|
+
<% unless item.tag2.SUBTITLE.blank? %>
|
29
|
+
<h2><%= h(item.tag2.SUBTITLE) %></h2>
|
30
|
+
<% end %>
|
31
|
+
<% unless item.tag2.TT3.blank? %>
|
32
|
+
<p><%= h(item.tag2.TT3) %></p>
|
33
|
+
<% end %>
|
34
|
+
<img src="<%= item.image_url %>"/>
|
35
|
+
<p>Download: <a href="<%= item.url %>">MP3</a> (<%= humanize_time(item.duration.to_i) %>, <%= humanize_size(item.file_size) %>)</p>
|
36
|
+
<p>Published <%= h(item.pub_date.to_formatted_s(:rfc822)) %></p>
|
37
|
+
<% unless item.keywords.blank? %>
|
38
|
+
<p><%= h(item.keywords) %></p>
|
39
|
+
<% end %>
|
40
|
+
</div>
|
41
|
+
<% } %>
|
42
|
+
</body>
|
43
|
+
</html>
|
@@ -0,0 +1,64 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!-- This file generated by Dropcaster <%= Dropcaster::VERSION%> -->
|
3
|
+
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
|
4
|
+
<channel>
|
5
|
+
<title><%= h(title) %></title>
|
6
|
+
<link><%= h(url) %></link>
|
7
|
+
<description><%= h(description) %></description>
|
8
|
+
<itunes:summary><%= h(description) %></itunes:summary>
|
9
|
+
<% unless language.blank? %>
|
10
|
+
<language><%= h(language) %></language>
|
11
|
+
<% end %>
|
12
|
+
<% unless copyright.blank? %>
|
13
|
+
<copyright><%= h(copyright) %></copyright>
|
14
|
+
<% end %>
|
15
|
+
<% unless subtitle.blank? %>
|
16
|
+
<itunes:subtitle><%= h(subtitle) %></itunes:subtitle>
|
17
|
+
<% end %>
|
18
|
+
<% unless author.blank? %>
|
19
|
+
<itunes:author><%= h(author) %></itunes:author>
|
20
|
+
<% end %>
|
21
|
+
<% unless owner.nil? %>
|
22
|
+
<itunes:owner>
|
23
|
+
<itunes:name><%= h(owner[:name]) %></itunes:name>
|
24
|
+
<itunes:email><%= h(owner[:email]) %></itunes:email>
|
25
|
+
</itunes:owner>
|
26
|
+
<% end %>
|
27
|
+
<% unless image_url.blank? %>
|
28
|
+
<itunes:image href="<%= image_url %>"/>
|
29
|
+
<% end %>
|
30
|
+
<% categories.each{|category| %>
|
31
|
+
<% if category.respond_to?(:each_line) %>
|
32
|
+
<itunes:category text="<%= h(category) %>"/>
|
33
|
+
<% else %>
|
34
|
+
<itunes:category text="<%= h(category.first) %>">
|
35
|
+
<itunes:category text="<%= h(category.last) %>"/>
|
36
|
+
</itunes:category>
|
37
|
+
<% end %>
|
38
|
+
<% } unless categories.blank? %>
|
39
|
+
<% unless explicit.blank? %>
|
40
|
+
<itunes:explicit><%= h(explicit) %></itunes:explicit>
|
41
|
+
<% end %>
|
42
|
+
<generator>Dropcaster <%= Dropcaster::VERSION%></generator>
|
43
|
+
<% items.each{|item| %>
|
44
|
+
<item>
|
45
|
+
<title><%= item.tag.title || item.tag2.TIT2%></title>
|
46
|
+
<itunes:author><%= h(item.tag2.TP1 || item.tag2.TPE1) %></itunes:author>
|
47
|
+
<% unless item.tag2.SUBTITLE.blank? %>
|
48
|
+
<itunes:subtitle><%= h(item.tag2.SUBTITLE) %></itunes:subtitle>
|
49
|
+
<% end %>
|
50
|
+
<% unless item.tag2.TT3.blank? %>
|
51
|
+
<itunes:summary><%= h(item.tag2.TT3) %></itunes:summary>
|
52
|
+
<% end %>
|
53
|
+
<itunes:image href="<%= item.image_url %>"/>
|
54
|
+
<enclosure url="<%= item.url %>" length="<%= item.file_size %>" type="audio/mp3"/>
|
55
|
+
<guid isPermaLink="false"><%= h(item.uuid) %></guid>
|
56
|
+
<pubDate><%= h(item.pub_date.to_formatted_s(:rfc822)) %></pubDate>
|
57
|
+
<itunes:duration><%= item.duration.to_i %></itunes:duration>
|
58
|
+
<% unless item.keywords.blank? %>
|
59
|
+
<itunes:keywords><%= h(item.keywords) %></itunes:keywords>
|
60
|
+
<% end %>
|
61
|
+
</item>
|
62
|
+
<% } %>
|
63
|
+
</channel>
|
64
|
+
</rss>
|
data/test/fixtures/channel.yml
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
-
:title:
|
2
|
-
:subtitle:
|
3
|
-
:url:
|
4
|
-
:language:
|
5
|
-
:copyright:
|
6
|
-
:author:
|
7
|
-
:description:
|
1
|
+
:title: All About Everything
|
2
|
+
:subtitle: A show about everything
|
3
|
+
:url: http://www.example.com/podcasts/everything/
|
4
|
+
:language: en-us
|
5
|
+
:copyright: © 2011 John Doe and Family
|
6
|
+
:author: John Doe
|
7
|
+
:description: All About Everything is a show about everything. Each week we dive into any subject known to man and talk about it as much as we can. Look for our Podcast in the iTunes Store!
|
8
8
|
:owner:
|
9
|
-
:name:
|
10
|
-
:email:
|
11
|
-
:image_url:
|
12
|
-
:categories:
|
13
|
-
|
9
|
+
:name: John Doe
|
10
|
+
:email: john.doe@example.com
|
11
|
+
:image_url: AllAboutEverything.jpg
|
12
|
+
:categories:
|
13
|
+
- [Technology, Gadgets]
|
14
|
+
- TV & Film
|
15
|
+
:explicit: No
|
@@ -0,0 +1,40 @@
|
|
1
|
+
{
|
2
|
+
"generator" : {
|
3
|
+
"name" : "Dropcaster",
|
4
|
+
"version" : "<%= Dropcaster::VERSION%>"
|
5
|
+
},
|
6
|
+
"channel" : {
|
7
|
+
"title" : "<%= title %>",
|
8
|
+
"link" : "<%= url %>",
|
9
|
+
"description" : "<%= description %>",
|
10
|
+
"language" : "<%= language %>",
|
11
|
+
"copyright" : "<%= copyright %>",
|
12
|
+
"subtitle" : "<%= subtitle %>",
|
13
|
+
"author" : "<%= author %>",
|
14
|
+
"owner" : {
|
15
|
+
"name" : "<%= owner[:name] %>",
|
16
|
+
"email" : "<%= owner[:email] %>"
|
17
|
+
},
|
18
|
+
"image" : "<%= image_url %>",
|
19
|
+
"categories": ["<% categories.join('\",\"') %>"],
|
20
|
+
"explicit" : "<%= explicit %>",
|
21
|
+
"items" : [<% items.collect{|item| %>{
|
22
|
+
"title" : "<%= item.tag.title || item.tag2.TIT2 %>",
|
23
|
+
"author" : "<%= item.tag2.TP1 || item.tag2.TPE1 %>",
|
24
|
+
"subtitle" : "<%= item.tag2.SUBTITLE %>",
|
25
|
+
"summary" : "<%= item.tag2.TT3 %>",
|
26
|
+
"image" : "<%= item.image_url %>",
|
27
|
+
"enclosure" : {
|
28
|
+
"url" : "<%= item.url %>",
|
29
|
+
"length" : "<%= item.file_size %>",
|
30
|
+
"type" : "audio/mp3"
|
31
|
+
},
|
32
|
+
"guid" : "<%= item.uuid %>",
|
33
|
+
"pubDate" : "<%= item.pub_date.to_formatted_s(:rfc822) %>",
|
34
|
+
"duration" : "<%= item.duration.to_i %>",
|
35
|
+
"keywords" : "<%= item.keywords %>"
|
36
|
+
}
|
37
|
+
<% }.join(',') %>
|
38
|
+
]
|
39
|
+
}
|
40
|
+
}
|
data/test/unit/test_app.rb
CHANGED
@@ -2,6 +2,7 @@ require 'helper'
|
|
2
2
|
require 'test_channel_xml'
|
3
3
|
require 'open3'
|
4
4
|
require 'uri'
|
5
|
+
require 'json'
|
5
6
|
|
6
7
|
#
|
7
8
|
# End-to-end test
|
@@ -28,6 +29,11 @@ class TestApp < TestChannelXML
|
|
28
29
|
assert_equal(test_link, channel.find('link').first.content)
|
29
30
|
end
|
30
31
|
|
32
|
+
def test_dir_only
|
33
|
+
channel = channel_node(%x[#{APP_SCRIPT} #{FIXTURES_DIR}])
|
34
|
+
assert_equal(1, channel.find('item').size)
|
35
|
+
end
|
36
|
+
|
31
37
|
def test_overwrite_enclosures_url
|
32
38
|
test_enclosures_url = 'http://www.example.com/foo/bar/episodes/'
|
33
39
|
channel = channel_node(%x[#{APP_SCRIPT} #{FIXTURE_ITUNES_MP3} --enclosures '#{test_enclosures_url}'])
|
@@ -57,21 +63,21 @@ class TestApp < TestChannelXML
|
|
57
63
|
|
58
64
|
def test_no_channel_file
|
59
65
|
Open3.popen3(APP_SCRIPT){|stdin, stdout, stderr|
|
60
|
-
assert_match(/No
|
66
|
+
assert_match(/ERROR: No channel file found/, stderr.read)
|
61
67
|
} unless Kernel.is_windows?
|
62
68
|
end
|
63
69
|
|
64
70
|
def test_overwrite_all
|
65
|
-
|
66
|
-
|
67
|
-
|
71
|
+
test_title = 'Bob and Alice in Wonderland'
|
72
|
+
test_link = 'http://www.example.com/bar/foot'
|
73
|
+
test_description = 'Testing commandline apps is really not that hard.'
|
68
74
|
|
69
|
-
|
75
|
+
channel = channel_node(%x[#{APP_SCRIPT} #{FIXTURE_ITUNES_MP3} --title '#{test_title}' --url '#{test_link}' --description '#{test_description}'])
|
70
76
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
77
|
+
assert_equal(test_title, channel.find('title').first.content)
|
78
|
+
assert_equal(test_link, channel.find('link').first.content)
|
79
|
+
assert_equal(test_description, channel.find('description').first.content)
|
80
|
+
assert_equal(test_description, channel.find('itunes:summary', NS_ITUNES).first.content)
|
75
81
|
end
|
76
82
|
|
77
83
|
def test_channel_template_not_found
|
@@ -80,5 +86,14 @@ class TestApp < TestChannelXML
|
|
80
86
|
} unless Kernel.is_windows?
|
81
87
|
end
|
82
88
|
|
89
|
+
#
|
90
|
+
# We supply an alternative channel template that produces JSON, parse it back and make some basic assertions on the results
|
91
|
+
#
|
92
|
+
def test_overwrite_channel_template
|
93
|
+
channel_template = File.join(FIXTURES_DIR, 'test_template.json.erb')
|
94
|
+
channel = JSON.parse(%x[#{APP_SCRIPT} #{FIXTURE_ITUNES_MP3} --channel-template #{channel_template}])
|
95
|
+
assert_equal("All About Everything", channel['channel']['title'])
|
96
|
+
end
|
97
|
+
|
83
98
|
# TODO --channel
|
84
99
|
end
|
data/test/unit/test_channel.rb
CHANGED
@@ -26,7 +26,48 @@ class TestChannel < Test::Unit::TestCase
|
|
26
26
|
assert_equal(@options[:owner][:email], owner[:email])
|
27
27
|
|
28
28
|
assert_equal(URI.join(@options[:url], @options[:image_url]).to_s, @channel.image_url)
|
29
|
-
|
30
|
-
|
29
|
+
|
30
|
+
categories = @channel.categories
|
31
|
+
assert_equal(@options[:categories], categories)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_channel_explicit_yes
|
35
|
+
assert_channel_explicit('Yes', true)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_channel_explicit_no
|
39
|
+
assert_channel_explicit('No', false)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_channel_explicit_nil
|
43
|
+
assert_channel_explicit(nil, nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_channel_explicit_clean
|
47
|
+
assert_channel_explicit('Clean', 'Clean')
|
48
|
+
end
|
49
|
+
|
50
|
+
def assert_channel_explicit(expected, value)
|
51
|
+
@options[:explicit] = value
|
52
|
+
channel = Dropcaster::Channel.new(FIXTURES_DIR, @options)
|
53
|
+
assert_equal(expected, channel.explicit)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_raise_on_missing_title
|
57
|
+
assert_raises Dropcaster::MissingAttributeError do
|
58
|
+
Dropcaster::Channel.new(FIXTURES_DIR, {:url => 'bar', :description => 'foobar'})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_raise_on_missing_url
|
63
|
+
assert_raises Dropcaster::MissingAttributeError do
|
64
|
+
Dropcaster::Channel.new(FIXTURES_DIR, {:title => 'foo', :description => 'foobar'})
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_raise_on_missing_description
|
69
|
+
assert_raises Dropcaster::MissingAttributeError do
|
70
|
+
Dropcaster::Channel.new(FIXTURES_DIR, {:title => 'foo', :url => 'bar'})
|
71
|
+
end
|
31
72
|
end
|
32
73
|
end
|
@@ -36,12 +36,14 @@ class TestChannelLocator < Test::Unit::TestCase
|
|
36
36
|
|
37
37
|
def test_current_directory
|
38
38
|
sources = File.join(TestChannelLocator.temp_dir, '.')
|
39
|
-
assert_location(sources)
|
39
|
+
assert_location(Pathname.new(sources).cleanpath) # Cleanup path before we compare
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_single_directory
|
43
|
-
|
44
|
-
|
43
|
+
source_dir = File.join(TestChannelLocator.temp_dir, 'single_dir')
|
44
|
+
Dir.mkdir(source_dir)
|
45
|
+
expected = File.join(TestChannelLocator.temp_dir, 'single_dir', Dropcaster::CHANNEL_YML)
|
46
|
+
assert_equal(expected, Dropcaster::ChannelFileLocator.locate(source_dir))
|
45
47
|
end
|
46
48
|
|
47
49
|
def test_array_of_files_same_dir
|
@@ -46,12 +46,8 @@ class TestChannelXML < Test::Unit::TestCase
|
|
46
46
|
assert_equal('false', guid['isPermaLink'])
|
47
47
|
assert_equal('77bf84447c0f69ce4a33a18b0ae1e030b82010de', guid.content)
|
48
48
|
|
49
|
-
assert_equal('
|
49
|
+
assert_equal('Wed, 05 Oct 2011 21:32:26 +0200', item.find('pubDate').first.content)
|
50
50
|
assert_equal('3', item.find('itunes:duration', NS_ITUNES).first.content)
|
51
|
-
|
52
|
-
# Not used in our fixture yet
|
53
|
-
# assert_equal('', item.find('itunes:subtitle', NS_ITUNES).first.content)
|
54
|
-
# assert_equal('', item.find('itunes:keywords', NS_ITUNES).first.content)
|
55
51
|
end
|
56
52
|
|
57
53
|
def test_attributes_mandatory
|
@@ -81,7 +77,13 @@ class TestChannelXML < Test::Unit::TestCase
|
|
81
77
|
assert_equal(@options[:owner][:email], owner.find('itunes:email', NS_ITUNES).first.content)
|
82
78
|
assert_equal(URI.join(@options[:url], @options[:image_url]).to_s, @channel.find('itunes:image', NS_ITUNES).first['href'])
|
83
79
|
|
84
|
-
|
85
|
-
|
80
|
+
categories = @channel.find('itunes:category', NS_ITUNES)
|
81
|
+
assert_not_nil(categories)
|
82
|
+
assert_equal(2, categories.size)
|
83
|
+
assert_equal('Technology', categories.first['text'])
|
84
|
+
assert_equal('Gadgets', categories.first.find('itunes:category', NS_ITUNES).first['text'])
|
85
|
+
assert_equal('TV & Film', categories.last['text'])
|
86
|
+
|
87
|
+
assert_equal(@options[:explicit] ? 'Yes' : 'No', @channel.find('itunes:explicit', NS_ITUNES).first.content)
|
86
88
|
end
|
87
89
|
end
|
data/test/unit/test_item.rb
CHANGED
@@ -11,7 +11,7 @@ class TestItem < Test::Unit::TestCase
|
|
11
11
|
assert_in_delta(3, @item.duration, 0.005)
|
12
12
|
assert_equal(58119, @item.file_size)
|
13
13
|
assert_equal('77bf84447c0f69ce4a33a18b0ae1e030b82010de', @item.uuid)
|
14
|
-
assert_equal(
|
14
|
+
assert_equal(1317843146, @item.pub_date.to_i)
|
15
15
|
assert_equal('test/fixtures/iTunes.mp3', @item.file_name)
|
16
16
|
end
|
17
17
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dropcaster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 4
|
10
|
+
version: 0.0.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- nerab
|
@@ -15,12 +15,11 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-10-
|
18
|
+
date: 2011-10-24 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
23
|
none: false
|
25
24
|
requirements:
|
26
25
|
- - ">="
|
@@ -29,12 +28,12 @@ dependencies:
|
|
29
28
|
segments:
|
30
29
|
- 0
|
31
30
|
version: "0"
|
32
|
-
|
33
|
-
|
31
|
+
type: :runtime
|
32
|
+
requirement: *id001
|
34
33
|
prerelease: false
|
34
|
+
name: ruby-mp3info
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
|
-
|
37
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
38
37
|
none: false
|
39
38
|
requirements:
|
40
39
|
- - ">="
|
@@ -43,12 +42,28 @@ dependencies:
|
|
43
42
|
segments:
|
44
43
|
- 0
|
45
44
|
version: "0"
|
46
|
-
|
47
|
-
|
45
|
+
type: :runtime
|
46
|
+
requirement: *id002
|
48
47
|
prerelease: false
|
48
|
+
name: activesupport
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - "="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 49
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
- 8
|
59
|
+
- 7
|
60
|
+
version: 0.8.7
|
50
61
|
type: :development
|
51
|
-
requirement:
|
62
|
+
requirement: *id003
|
63
|
+
prerelease: false
|
64
|
+
name: rake
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
52
67
|
none: false
|
53
68
|
requirements:
|
54
69
|
- - ~>
|
@@ -59,12 +74,12 @@ dependencies:
|
|
59
74
|
- 0
|
60
75
|
- 0
|
61
76
|
version: 1.0.0
|
62
|
-
|
63
|
-
|
77
|
+
type: :development
|
78
|
+
requirement: *id004
|
64
79
|
prerelease: false
|
80
|
+
name: bundler
|
65
81
|
- !ruby/object:Gem::Dependency
|
66
|
-
|
67
|
-
requirement: &id004 !ruby/object:Gem::Requirement
|
82
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
68
83
|
none: false
|
69
84
|
requirements:
|
70
85
|
- - ~>
|
@@ -75,12 +90,12 @@ dependencies:
|
|
75
90
|
- 6
|
76
91
|
- 4
|
77
92
|
version: 1.6.4
|
78
|
-
|
79
|
-
|
93
|
+
type: :development
|
94
|
+
requirement: *id005
|
80
95
|
prerelease: false
|
96
|
+
name: jeweler
|
81
97
|
- !ruby/object:Gem::Dependency
|
82
|
-
|
83
|
-
requirement: &id005 !ruby/object:Gem::Requirement
|
98
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
84
99
|
none: false
|
85
100
|
requirements:
|
86
101
|
- - ">="
|
@@ -89,12 +104,12 @@ dependencies:
|
|
89
104
|
segments:
|
90
105
|
- 0
|
91
106
|
version: "0"
|
92
|
-
|
93
|
-
|
107
|
+
type: :development
|
108
|
+
requirement: *id006
|
94
109
|
prerelease: false
|
110
|
+
name: libxml-ruby
|
95
111
|
- !ruby/object:Gem::Dependency
|
96
|
-
|
97
|
-
requirement: &id006 !ruby/object:Gem::Requirement
|
112
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
98
113
|
none: false
|
99
114
|
requirements:
|
100
115
|
- - ">="
|
@@ -103,9 +118,24 @@ dependencies:
|
|
103
118
|
segments:
|
104
119
|
- 0
|
105
120
|
version: "0"
|
121
|
+
type: :development
|
122
|
+
requirement: *id007
|
123
|
+
prerelease: false
|
106
124
|
name: rdoc
|
107
|
-
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
version_requirements: &id008 !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
hash: 3
|
132
|
+
segments:
|
133
|
+
- 0
|
134
|
+
version: "0"
|
135
|
+
type: :development
|
136
|
+
requirement: *id008
|
108
137
|
prerelease: false
|
138
|
+
name: json
|
109
139
|
description: Dropcaster is a podcast feed generator for the command line. It is most simple to use with Dropbox, but works equally well with any other hoster.
|
110
140
|
email: nerab@gmx.at
|
111
141
|
executables:
|
@@ -142,10 +172,13 @@ files:
|
|
142
172
|
- lib/dropcaster/errors.rb
|
143
173
|
- lib/dropcaster/hashkeys.rb
|
144
174
|
- lib/dropcaster/item.rb
|
145
|
-
-
|
175
|
+
- lib/dropcaster/log_formatter.rb
|
176
|
+
- templates/channel.html.erb
|
177
|
+
- templates/channel.rss.erb
|
146
178
|
- test/extensions/windows.rb
|
147
179
|
- test/fixtures/channel.yml
|
148
180
|
- test/fixtures/iTunes.mp3
|
181
|
+
- test/fixtures/test_template.json.erb
|
149
182
|
- test/helper.rb
|
150
183
|
- test/unit/test_app.rb
|
151
184
|
- test/unit/test_channel.rb
|
data/templates/iTunes.rss.erb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
-
<!-- This file was automatically generated by Dropcaster <%= Dropcaster::VERSION%> -->
|
3
|
-
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
|
4
|
-
<channel>
|
5
|
-
<title><%= title %></title>
|
6
|
-
<link><%= url %></link>
|
7
|
-
<description><%= description %></description>
|
8
|
-
<itunes:summary><%= description %></itunes:summary>
|
9
|
-
<% unless language.blank? %>
|
10
|
-
<language><%= language %></language>
|
11
|
-
<% end %>
|
12
|
-
<% unless copyright.blank? %>
|
13
|
-
<copyright><%= copyright %></copyright>
|
14
|
-
<% end %>
|
15
|
-
<% unless subtitle.blank? %>
|
16
|
-
<itunes:subtitle><%= subtitle %></itunes:subtitle>
|
17
|
-
<% end %>
|
18
|
-
<% unless author.blank? %>
|
19
|
-
<itunes:author><%= author %></itunes:author>
|
20
|
-
<% end %>
|
21
|
-
<% unless owner.nil? %>
|
22
|
-
<itunes:owner>
|
23
|
-
<itunes:name><%= owner[:name] %></itunes:name>
|
24
|
-
<itunes:email><%= owner[:email] %></itunes:email>
|
25
|
-
</itunes:owner>
|
26
|
-
<% end %>
|
27
|
-
<% unless image_url.blank? %>
|
28
|
-
<itunes:image href="<%= image_url %>"/>
|
29
|
-
<% end %>
|
30
|
-
<% categories.each{|category| %>
|
31
|
-
<itunes:category text="<%= category %>"/>
|
32
|
-
<% } %>
|
33
|
-
<% unless explicit.blank? %>
|
34
|
-
<itunes:explicit><%= explicit %></itunes:explicit>
|
35
|
-
<% end %>
|
36
|
-
<% items.each{|item| %>
|
37
|
-
<item>
|
38
|
-
<title><%= item.tag.title || item.tag2.TIT2%></title>
|
39
|
-
<itunes:author><%= item.tag2.TP1 || item.tag2.TPE1 %></itunes:author>
|
40
|
-
<% unless item.tag2.SUBTITLE.blank? %>
|
41
|
-
<itunes:subtitle><%= item.tag2.SUBTITLE %></itunes:subtitle>
|
42
|
-
<% end %>
|
43
|
-
<% unless item.tag2.TT3.blank? %>
|
44
|
-
<itunes:summary><%= item.tag2.TT3 %></itunes:summary>
|
45
|
-
<% end %>
|
46
|
-
<itunes:image href="<%= item.image_url %>"/>
|
47
|
-
<enclosure url="<%= item.url %>" length="<%= item.file_size %>" type="audio/mp3"/>
|
48
|
-
<guid isPermaLink="false"><%= item.uuid %></guid>
|
49
|
-
<pubDate><%= item.pub_date.to_formatted_s(:rfc822) %></pubDate>
|
50
|
-
<itunes:duration><%= item.duration.to_i %></itunes:duration>
|
51
|
-
<% unless item.keywords.blank? %>
|
52
|
-
<itunes:keywords><%= item.keywords %></itunes:keywords>
|
53
|
-
<% end %>
|
54
|
-
</item>
|
55
|
-
<% } %>
|
56
|
-
</channel>
|
57
|
-
</rss>
|