dropcaster 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -0
- data/README.md +62 -28
- data/Rakefile +2 -2
- data/TODO +3 -1
- data/VERSION +1 -1
- data/bin/dropcaster +69 -34
- data/doc/sample-channel.yml +77 -0
- data/doc/sample-sidecar.yml +19 -0
- data/dropcaster.gemspec +87 -0
- data/lib/dropcaster/channel.rb +27 -6
- data/lib/dropcaster/channel_file_locator.rb +48 -0
- data/lib/dropcaster/errors.rb +15 -7
- data/lib/dropcaster/item.rb +1 -1
- data/lib/dropcaster.rb +2 -0
- data/test/helper.rb +7 -0
- data/test/unit/test_app.rb +59 -0
- data/test/unit/test_channel.rb +20 -31
- data/test/unit/test_channel_locator.rb +91 -0
- data/test/unit/test_channel_xml.rb +69 -0
- data/test/unit/test_item.rb +1 -1
- metadata +29 -8
- /data/{test/fixtures → doc}/infoPane.png +0 -0
- /data/{test/fixtures → doc}/lyricsPane.png +0 -0
- /data/{test/fixtures → doc}/videoPane.png +0 -0
- /data/test/fixtures/{test_channel.yml → channel.yml} +0 -0
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -8,15 +8,19 @@ GEM
|
|
8
8
|
git (>= 1.2.5)
|
9
9
|
rake
|
10
10
|
libxml-ruby (2.2.1)
|
11
|
+
libxml-ruby (2.2.1-x86-mingw32)
|
11
12
|
rake (0.9.2)
|
13
|
+
rdoc (3.9.3)
|
12
14
|
ruby-mp3info (0.6.15)
|
13
15
|
|
14
16
|
PLATFORMS
|
15
17
|
ruby
|
18
|
+
x86-mingw32
|
16
19
|
|
17
20
|
DEPENDENCIES
|
18
21
|
activesupport
|
19
22
|
bundler (~> 1.0.0)
|
20
23
|
jeweler (~> 1.6.4)
|
21
24
|
libxml-ruby
|
25
|
+
rdoc
|
22
26
|
ruby-mp3info
|
data/README.md
CHANGED
@@ -1,36 +1,60 @@
|
|
1
1
|
Dropcaster - Simple Podcast Publishing with Dropbox
|
2
2
|
===================================================
|
3
|
-
|
4
|
-
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.
|
3
|
+
[Dropcaster](http://nerab.github.com/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.
|
5
4
|
|
6
5
|
Author: Nicolas E. Rabenau <nerab@gmx.at>
|
7
6
|
|
8
7
|
What is the problem Dropcaster is trying to solve?
|
9
8
|
==================================================
|
10
|
-
|
11
9
|
You have a number of podcast episodes that you would like to publish as a feed. Nothing else - no fancy website, no stats, nothing but the pure podcast.
|
12
10
|
|
13
|
-
With Dropcaster, you simply put the mp3 files into the Public folder of your [Dropbox](http://www.dropbox.com/). Then run the Dropcaster script that generates the feed, writing it to a file in your Dropbox, e.g. index.rss. All mp3 files in the Public folder of your Dropbox are already accessible via HTTP, and so will the RSS file. You can then take the RSS file's URL and publish it (again, this is because any file in the Public folder of
|
11
|
+
With Dropcaster, you simply put the mp3 files into the Public folder of your [Dropbox](http://www.dropbox.com/). Then run the Dropcaster script that generates the feed, writing it to a file in your Dropbox, e.g. index.rss. All mp3 files in the Public folder of your Dropbox are already accessible via HTTP, and so will the RSS file. You can then take the RSS file's URL and publish it (again, this is because any file in the Public folder of a Dropbox automatically gets a public, HTTP-accessible URL).
|
14
12
|
|
15
|
-
The feed URL can be consumed
|
13
|
+
The feed URL can be consumed by any podcatcher, e.g. [iTunes](http://www.apple.com/itunes/) or [Juice](http://juicereceiver.sourceforge.net/).
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
Installation
|
16
|
+
============
|
17
|
+
To get started, use RubyGems to install Dropcaster:
|
18
|
+
|
19
|
+
$ gem install dropcaster
|
20
|
+
|
21
|
+
With Dropcaster installed, you can use the `dropcaster` command to generate a new podcast feed document.
|
22
|
+
|
23
|
+
Basic Usage
|
24
|
+
===========
|
25
|
+
Once Dropcaster is installed, the only two other things you will need are a channel definition and one or more mp3 files to publish.
|
26
|
+
|
27
|
+
Let's start with the channel definition. It is a simple [YAML](http://yaml.org/) file that holds the general information about your podcast channel. According to the [RSS 2.0 spec](http://feedvalidator.org/docs/rss2.html#requiredChannelElements), the only mandatory information that your channel absolutely needs are a title, a description and a link to a web site where the channel belongs to.
|
28
|
+
|
29
|
+
The simplest channel file looks like this:
|
30
|
+
|
31
|
+
:title: 'All About Everything'
|
32
|
+
:subtitle: 'A show about everything'
|
33
|
+
:url: 'http://www.example.com/podcasts/everything/index.html'
|
19
34
|
|
20
|
-
|
35
|
+
Store this file as channel.yml in the same directory where the mp3 files of your podcast reside. The channel definition is expected to be present in the same directory as your mp3 files, but this can be overridden using a command line switch. You can find a [more elaborate example](http://github.com/nerab/dropcaster/blob/master/doc/sample-channel.yml) for the channel definition in the doc folder of the Dropcaster gem. You can find it by running `gem open dropcaster`. Instead of writing these title, subtitle, etc. to the channel.yml, you may also spedify them on the command line. For details, just run
|
21
36
|
|
22
|
-
|
37
|
+
$ dropcaster --help
|
23
38
|
|
24
|
-
|
39
|
+
Now that we have the podcast channel defined, we need at least one episode (an audio file) in it. From Dropcaster's perspective, it does not matter how the episode was produced, but the critical information is the meta data in the mp3 file, because that is the authoritative source for the episode information. Almost all audio editors can write metadata, usually called ID3 tags. Dropcaster reads these tags from the mp3 files and fills the item element in the feed (that's how an episode is defined, technically) from it.
|
25
40
|
|
26
|
-
|
41
|
+
With all required pieces in place, we could generate the podcast feed. Just before we do that, we will inspect the feed by running the following commands:
|
42
|
+
|
43
|
+
$ cd ~/Dropbox/Public
|
44
|
+
$ dropcaster
|
45
|
+
|
46
|
+
(The above lines assume that you are using Dropbox, and that there is at least one mp3 file in ~/Dropbox/Public).
|
47
|
+
|
48
|
+
Dropcaster will print the feed to standard-out, without writing it to disk. When you are happy with the results, call Dropcaster again, but redirect the output to a file, this time:
|
49
|
+
|
50
|
+
$ dropcaster > index.rss
|
51
|
+
|
52
|
+
If all went well, you will now have a valid podcast feed in your Dropbox, listing all mp3 files as podcast episodes. Please see the section [Publish Your Feed] for details on how to find the public URL of your feed.
|
27
53
|
|
28
54
|
Use Cases
|
29
55
|
=========
|
30
|
-
|
31
56
|
Publish a New Episode
|
32
57
|
---------------------
|
33
|
-
|
34
58
|
1. Drop the mp3 file into the Dropbox Public folder (e.g. ~/Dropbox/Public), and then run the following command in the directory where the mp3 files reside:
|
35
59
|
|
36
60
|
$ dropcaster > index.rss
|
@@ -39,28 +63,33 @@ Publish a New Episode
|
|
39
63
|
|
40
64
|
Delete an Episode
|
41
65
|
-----------------
|
42
|
-
|
43
66
|
Remove the mp3 you want to delete from the Dropbox Public folder, and then run the following command in the directory where the remaining mp3 files reside:
|
44
67
|
|
45
68
|
$ dropcaster > index.rss
|
46
69
|
|
47
70
|
Replace an Episode With an Updated File
|
48
71
|
---------------------------------------
|
49
|
-
|
50
72
|
In the Dropbox Public folder, replace the mp3 you want to update with a new version, and then run the following command in the directory where the mp3 files reside:
|
51
73
|
|
52
74
|
$ dropcaster > index.rss
|
53
75
|
|
54
76
|
Publish Your Feed
|
55
77
|
-----------------
|
56
|
-
|
57
78
|
1. Re-generate the feed to make sure the it is up to date (see above):
|
58
79
|
|
59
80
|
$ dropcaster > index.rss
|
60
|
-
|
81
|
+
|
61
82
|
1. In your Dropbox Public folder, right-click the index.rss and select "Dropbox / Copy public link". This copies the public, HTTP-addressable link to your podcast into the clipboard.
|
62
83
|
1. Publish this link and tell people to subscribe to it.
|
63
84
|
|
85
|
+
Generate a Podcast Feed for a Subset of the Available MP3 Files
|
86
|
+
---------------------------------------------------------------
|
87
|
+
Dropcaster accepts any number of files or directories as episodes. For directories, all files ending in .mp3 will be included. For advanced filtering, you can use regular shell patterns to further specify which files will be included. These patterns will be resolved by the shell itself (e.g. bash), and not by Dropcaster.
|
88
|
+
|
89
|
+
For example, in order to generate a feed that only publishes MP3 files where the name starts with 'A', call Dropcaster like this:
|
90
|
+
|
91
|
+
$ dropcaster A*.mp3 > index.rss
|
92
|
+
|
64
93
|
Publish More than One Feed
|
65
94
|
--------------------------
|
66
95
|
|
@@ -79,26 +108,32 @@ Include Episodes From Two Subdirectories Into a Single Feed
|
|
79
108
|
|
80
109
|
$ dropcaster project1 project2 > index.rss
|
81
110
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
111
|
+
Advanced features
|
112
|
+
=================
|
113
|
+
Sidecar files
|
114
|
+
-------------
|
115
|
+
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.
|
86
116
|
|
87
|
-
|
117
|
+
Dropcaster will only write the sidecar file if the appropriate command line option was passed, and it will use the information in it only for generating new files like the index.rss. It will not write back to mp3 files.
|
88
118
|
|
89
119
|
A Note on iTunes
|
90
|
-
|
91
|
-
|
120
|
+
----------------
|
92
121
|
The generated XML file contains all elements required for iTunes. However, Dropcaster will not notify the iTunes store about new episodes.
|
93
122
|
|
94
123
|
Using Dropcaster Without Dropbox
|
95
|
-
|
96
|
-
|
124
|
+
--------------------------------
|
97
125
|
The whole concept of Dropcaster works perfectly fine without Dropbox. Just run the Dropcaster script in a directory of mp3 files and upload the files as well as the generated index.rss to a web server. Leave the relative position of the index and mp3 files as is, otherwise the path to the mp3 files in index.rss will become invalid.
|
98
126
|
|
127
|
+
Episode Identifier (uuid)
|
128
|
+
-------------------------
|
129
|
+
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 changes), the episode will get a new UUID, and any podcatcher will fetch the episode again (which is what you want, in most cases).
|
130
|
+
|
131
|
+
Modifying the sidecar file does not change the UUID, because it only affects the feed and not the episode itself.
|
132
|
+
|
99
133
|
Contributing to Dropcaster
|
100
134
|
==========================
|
101
|
-
|
135
|
+
Dropcaster is hosted at [Github](http://github.com/nerab/dropcaster):
|
136
|
+
|
102
137
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
103
138
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
104
139
|
* Fork the project
|
@@ -109,5 +144,4 @@ Contributing to Dropcaster
|
|
109
144
|
|
110
145
|
Copyright
|
111
146
|
=========
|
112
|
-
|
113
147
|
Copyright (c) 2011 Nicolas E. Rabenau. See LICENSE.txt for further details.
|
data/Rakefile
CHANGED
data/TODO
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
* Take the the channel's URL as enclosure_url and make enclosure_url an optional parameter
|
2
|
+
* In verbose mode, log warnings to STDERR for all the opinionated decisions and fallbacks that Dropcaster takes
|
1
3
|
* Allow overriding the path to the ERB template (and document the available template variables)
|
2
4
|
* Support a tree of iTunes categories: <itunes:category text="Technology"><itunes:category text="Gadgets"/></itunes:category>
|
3
5
|
* If an index.html.erb is present, generate an index.html from it
|
@@ -5,7 +7,7 @@
|
|
5
7
|
* Print warnings when one of the specs from http://www.apple.com/itunes/podcasts/specs.html is violated
|
6
8
|
* Allow muting of the iTunes warnings (see above) using a commandline switch
|
7
9
|
* Implement sidecar files
|
8
|
-
* Support other file types than mp3
|
10
|
+
* Support other file types than mp3 (e.g. ogg or pdf)
|
9
11
|
* Treat lyrics as plain text for the episode page, and with an optional command line switch we could also support markdown etc.
|
10
12
|
* 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.
|
11
13
|
* 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?
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/bin/dropcaster
CHANGED
@@ -8,20 +8,17 @@ require 'yaml'
|
|
8
8
|
help = <<HELP
|
9
9
|
Dropcaster is a podcast feed generator for the command line.
|
10
10
|
|
11
|
+
Author: Nicolas E. Rabenau nerab@gmx.at
|
12
|
+
Homepage: http://nerab.github.com/dropcaster/
|
13
|
+
|
11
14
|
Basic Usage:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
dropcaster [<dir>...] Prints a podcast feed document for the files in
|
21
|
-
the directory specified as argument. The channel
|
22
|
-
definition is read from the current directory.
|
23
|
-
|
24
|
-
Options:
|
15
|
+
|
16
|
+
dropcaster Prints a podcast feed document for the mp3 files in the current directory.
|
17
|
+
dropcaster [FILE]... Prints a podcast feed document for FILES
|
18
|
+
dropcaster [DIR]... Prints a podcast feed document for the mp3 files in DIR
|
19
|
+
|
20
|
+
Options:
|
21
|
+
|
25
22
|
HELP
|
26
23
|
|
27
24
|
def usage
|
@@ -31,18 +28,51 @@ end
|
|
31
28
|
require 'optparse'
|
32
29
|
require 'dropcaster'
|
33
30
|
|
34
|
-
|
31
|
+
options = Hash.new
|
32
|
+
options[:verbose] = false
|
33
|
+
options[:auto_detect_channel_file] = true
|
35
34
|
|
36
|
-
options = {}
|
37
35
|
opts = OptionParser.new do |opts|
|
38
36
|
opts.banner = help
|
39
37
|
|
40
|
-
opts.on("--
|
41
|
-
|
38
|
+
opts.on("--verbose", "Verbose mode - displays additional diagnostic information") do |file|
|
39
|
+
options[:verbose] = true
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("--channel FILE", "Read the channel definition from FILE instead of channel.yml in the current directory.") do |file|
|
43
|
+
begin
|
44
|
+
STDERR.puts "Reading channel definition from #{file}" if options[:verbose]
|
45
|
+
options = YAML.load_file(file).merge(options)
|
46
|
+
options[:auto_detect_channel_file] = false
|
47
|
+
rescue
|
48
|
+
STDERR.puts "Error loading channel definition: #{$!.message}"
|
49
|
+
STDERR.puts $!.backtrace if options[:verbose]
|
50
|
+
exit(1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("--title STRING", "Use STRING as the channel's title. Overrides settings read from channel definition file.") do |title|
|
55
|
+
STDERR.puts "Setting channel title to '#{title}' via command line" if options[:verbose]
|
56
|
+
options[:title] = title
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on("--url URL", "Use URL as the channel's url. Overrides settings read from channel definition file.") do |url|
|
60
|
+
STDERR.puts "Setting channel URL to '#{url}' via command line" if options[:verbose]
|
61
|
+
options[:url] = url
|
42
62
|
end
|
43
63
|
|
44
|
-
opts.on("--
|
45
|
-
options[:
|
64
|
+
opts.on("--description STRING", "Use STRING as the channel's description. Overrides settings read from channel definition file.") do |description|
|
65
|
+
STDERR.puts "Setting channel description to '#{description}' via command line" if options[:verbose]
|
66
|
+
options[:description] = description
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on("--enclosure_base URL", "Use URL as base URL for the channel's enclosures. Overrides settings read from channel definition file.") do |enclosure_base|
|
70
|
+
STDERR.puts "Setting enclosure base to '#{enclosure_base}' via command line" if options[:verbose]
|
71
|
+
options[:enclosure_base] = enclosure_base
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on("--channel-template FILE", "Use FILE as template for generating the channel feed. Overrides the default that comes with Dropcaster.") do |file|
|
75
|
+
options[:template] = file
|
46
76
|
end
|
47
77
|
|
48
78
|
opts.on("--version", "Display current version") do
|
@@ -52,25 +82,30 @@ opts = OptionParser.new do |opts|
|
|
52
82
|
end
|
53
83
|
|
54
84
|
opts.parse!
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
85
|
+
sources = ARGV.blank? ? '.' : ARGV
|
86
|
+
|
87
|
+
if options[:auto_detect_channel_file]
|
88
|
+
# There was no channel file specified, so we try to load channel.yml from sources dir
|
89
|
+
channel_file = Dropcaster::ChannelFileLocator.locate(sources)
|
90
|
+
|
91
|
+
if File.exists?(channel_file)
|
92
|
+
STDERR.puts "Auto-detected channel file at #{channel_file}" if options[:verbose]
|
93
|
+
options_from_yaml = YAML.load_file(channel_file)
|
94
|
+
options = options_from_yaml.merge(options)
|
95
|
+
else
|
96
|
+
STDERR.puts "No #{channel_file} found."
|
97
|
+
STDERR.puts usage
|
98
|
+
exit(1) # No way to continue without a channel definition
|
99
|
+
end
|
62
100
|
end
|
63
101
|
|
64
|
-
if
|
65
|
-
src_dir = '.'
|
66
|
-
else
|
67
|
-
src_dir = ARGV
|
68
|
-
end
|
102
|
+
STDERR.puts "Generating the channel with these options: #{options.inspect}" if options[:verbose]
|
69
103
|
|
70
104
|
begin
|
71
|
-
puts Dropcaster::Channel.new(
|
105
|
+
puts Dropcaster::Channel.new(sources, options).to_rss
|
72
106
|
rescue
|
73
|
-
puts "Error: #{$!.message}"
|
74
|
-
puts usage
|
107
|
+
STDERR.puts "Error generating the channel feed: #{$!.message}"
|
108
|
+
STDERR.puts usage
|
109
|
+
STDERR.puts $!.backtrace if options[:verbose]
|
75
110
|
exit(1)
|
76
111
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# A sample RSS channel definition
|
3
|
+
#
|
4
|
+
# This file is read by Dropcaster
|
5
|
+
# http://github.com/nerab/dropcaster
|
6
|
+
#
|
7
|
+
# It defines the properties of your podcast channel.
|
8
|
+
#
|
9
|
+
|
10
|
+
#
|
11
|
+
# Title (name) of the podcast
|
12
|
+
#
|
13
|
+
:title: 'All About Everything'
|
14
|
+
|
15
|
+
#
|
16
|
+
# Short description of the podcast (a few words)
|
17
|
+
#
|
18
|
+
:subtitle: 'A show about everything'
|
19
|
+
|
20
|
+
#
|
21
|
+
# URL to the podcast.
|
22
|
+
#
|
23
|
+
:url: 'http://www.example.com/podcasts/everything/index.html'
|
24
|
+
|
25
|
+
#
|
26
|
+
# Base URL for enclosure links
|
27
|
+
#
|
28
|
+
:enclosure_base: 'http://www.example.com/podcasts/everything'
|
29
|
+
|
30
|
+
#
|
31
|
+
# Language of the podcast - ISO 639-1 Alpha-2 list (two-letter language codes, some with possible modifiers, such as "en-us").
|
32
|
+
#
|
33
|
+
:language: 'en-us'
|
34
|
+
|
35
|
+
#
|
36
|
+
# Not visible in iTunes, but useful as a statement in the feed
|
37
|
+
#
|
38
|
+
:copyright: '© 2011 John Doe & Family'
|
39
|
+
|
40
|
+
#
|
41
|
+
# Author / creator of the podcast. In iTunes, it is displayed in the artist column.
|
42
|
+
#
|
43
|
+
:author: 'John Doe'
|
44
|
+
|
45
|
+
#
|
46
|
+
# Longer description of the podcast
|
47
|
+
#
|
48
|
+
: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!'
|
49
|
+
|
50
|
+
#
|
51
|
+
# Contact information of the owner of the podcast. Not be publicly displayed in iTunes.
|
52
|
+
#
|
53
|
+
:owner:
|
54
|
+
:name: 'John Doe'
|
55
|
+
:email: 'john.doe@example.com'
|
56
|
+
|
57
|
+
#
|
58
|
+
# iTunes prefers square .jpg images that are at least 600 x 600 pixels
|
59
|
+
#
|
60
|
+
:image_url: 'http://example.com/podcasts/everything/AllAboutEverything.jpg'
|
61
|
+
|
62
|
+
#
|
63
|
+
# Category / categories of the podcast
|
64
|
+
#
|
65
|
+
# For iTunes, see http://www.apple.com/itunes/podcasts/specs.html#categories for applicable values
|
66
|
+
#
|
67
|
+
# Examples:
|
68
|
+
# :categories: 'Technology'
|
69
|
+
# :categories: ['Technology', 'Gadgets']
|
70
|
+
# :categories: ['TV & Film', ['Technology', 'Gadgets']]
|
71
|
+
|
72
|
+
:categories: ['Technology', 'Gadgets']
|
73
|
+
|
74
|
+
#
|
75
|
+
# Yes, No or Clean, see http://www.apple.com/itunes/podcasts/specs.html#explicit
|
76
|
+
#
|
77
|
+
:explicit: 'No'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# Sample sidecar file
|
3
|
+
#
|
4
|
+
# If this file is present side-by-side with an mp3 file, and if it has the same name as the mp3 (but with
|
5
|
+
# an extension of yml or yaml), then any setting made in this file will be used instead of the value from the mp3.
|
6
|
+
#
|
7
|
+
:title: 'Title of the episode'
|
8
|
+
:author: 'Author of the episode'
|
9
|
+
:subtitle: 'Subtitle of the episode'
|
10
|
+
:description: 'Desciption and summary of the episode'
|
11
|
+
:image_url: 'URL to an image specific for this episode'
|
12
|
+
:enclosure:
|
13
|
+
:url: 'URL where this episode can be downloaded'
|
14
|
+
:length: 42 # length of the episode in milliseconds
|
15
|
+
:type: 'usually audio/mp3'
|
16
|
+
:guid: 'Globally unique id of the episode'
|
17
|
+
:pubDate: 'Date as per RFC 2822; e.g. Wed, 15 Jun 2005 19:00:00 GMT'
|
18
|
+
:duration: 'Either HH:MM:SS, H:MM:SS, MM:SS, M:SS, or SS'
|
19
|
+
:keywords: ['ruby', 'rails', 'podcast'] # up to 12 text keywords
|
data/dropcaster.gemspec
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dropcaster}
|
8
|
+
s.version = "0.0.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = [%q{nerab}]
|
12
|
+
s.date = %q{2011-10-05}
|
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
|
+
s.email = %q{nerab@gmx.at}
|
15
|
+
s.executables = [%q{lstags}, %q{dropcaster}, %q{dropcaster}, %q{lstags}]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE.txt",
|
18
|
+
"README.md",
|
19
|
+
"TODO"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
"Gemfile",
|
24
|
+
"Gemfile.lock",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"TODO",
|
29
|
+
"VERSION",
|
30
|
+
"bin/dropcaster",
|
31
|
+
"bin/lstags",
|
32
|
+
"doc/infoPane.png",
|
33
|
+
"doc/lyricsPane.png",
|
34
|
+
"doc/sample-channel.yml",
|
35
|
+
"doc/sample-sidecar.yml",
|
36
|
+
"doc/videoPane.png",
|
37
|
+
"dropcaster.gemspec",
|
38
|
+
"lib/dropcaster.rb",
|
39
|
+
"lib/dropcaster/channel.rb",
|
40
|
+
"lib/dropcaster/channel_file_locator.rb",
|
41
|
+
"lib/dropcaster/errors.rb",
|
42
|
+
"lib/dropcaster/hashkeys.rb",
|
43
|
+
"lib/dropcaster/item.rb",
|
44
|
+
"templates/channel.rss.erb",
|
45
|
+
"test/fixtures/channel.yml",
|
46
|
+
"test/fixtures/iTunes.mp3",
|
47
|
+
"test/helper.rb",
|
48
|
+
"test/unit/test_app.rb",
|
49
|
+
"test/unit/test_channel.rb",
|
50
|
+
"test/unit/test_channel_locator.rb",
|
51
|
+
"test/unit/test_channel_xml.rb",
|
52
|
+
"test/unit/test_item.rb"
|
53
|
+
]
|
54
|
+
s.homepage = %q{http://github.com/nerab/dropcaster}
|
55
|
+
s.licenses = [%q{MIT}]
|
56
|
+
s.require_paths = [%q{lib}]
|
57
|
+
s.rubygems_version = %q{1.8.9}
|
58
|
+
s.summary = %q{Simple Podcast Publishing with Dropbox}
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
s.specification_version = 3
|
62
|
+
|
63
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
64
|
+
s.add_runtime_dependency(%q<ruby-mp3info>, [">= 0"])
|
65
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 0"])
|
66
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
67
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
68
|
+
s.add_development_dependency(%q<libxml-ruby>, [">= 0"])
|
69
|
+
s.add_development_dependency(%q<rdoc>, [">= 0"])
|
70
|
+
else
|
71
|
+
s.add_dependency(%q<ruby-mp3info>, [">= 0"])
|
72
|
+
s.add_dependency(%q<activesupport>, [">= 0"])
|
73
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
74
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
75
|
+
s.add_dependency(%q<libxml-ruby>, [">= 0"])
|
76
|
+
s.add_dependency(%q<rdoc>, [">= 0"])
|
77
|
+
end
|
78
|
+
else
|
79
|
+
s.add_dependency(%q<ruby-mp3info>, [">= 0"])
|
80
|
+
s.add_dependency(%q<activesupport>, [">= 0"])
|
81
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
82
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
83
|
+
s.add_dependency(%q<libxml-ruby>, [">= 0"])
|
84
|
+
s.add_dependency(%q<rdoc>, [">= 0"])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
data/lib/dropcaster/channel.rb
CHANGED
@@ -2,14 +2,28 @@ require 'erb'
|
|
2
2
|
require 'uri'
|
3
3
|
|
4
4
|
module Dropcaster
|
5
|
+
#
|
6
|
+
# Represents a podcast feed in the RSS 2.0 format
|
7
|
+
#
|
5
8
|
class Channel < DelegateClass(Hash)
|
6
9
|
include HashKeys
|
7
10
|
|
11
|
+
# Instantiate a new Channel object. +sources+ must be present and can be a String or Array
|
12
|
+
# of Strings, pointing to a one or more directories or MP3 files.
|
13
|
+
#
|
14
|
+
# +options+ is a hash with all attributes for the channel. The following attributes are
|
15
|
+
# mandatory when a new channel is created:
|
16
|
+
#
|
17
|
+
# * <tt>:title</tt> - Title (name) of the podcast
|
18
|
+
# * <tt>:url</tt> - URL to the podcast
|
19
|
+
# * <tt>:description</tt> - Short description of the podcast (a few words)
|
20
|
+
# * <tt>:enclosure_base</tt> - Base URL for enclosure links
|
21
|
+
#
|
8
22
|
def initialize(sources, options)
|
9
23
|
super(Hash.new)
|
10
24
|
|
11
|
-
# Assert
|
12
|
-
[:title, :url, :description].each{|attr|
|
25
|
+
# Assert mandatory options
|
26
|
+
[:title, :url, :description, :enclosure_base].each{|attr|
|
13
27
|
raise MissingAttributeError.new(attr) if options[attr].blank?
|
14
28
|
}
|
15
29
|
|
@@ -29,10 +43,18 @@ module Dropcaster
|
|
29
43
|
@index_template = ERB.new(File.new(File.join(File.dirname(__FILE__), '..', '..', 'templates', 'channel.rss.erb')), 0, "%<>")
|
30
44
|
end
|
31
45
|
|
46
|
+
#
|
47
|
+
# Returns this channel as an RSS representation. The actual rendering is done with the help
|
48
|
+
# of an ERB template. By default, it is expected as ../../templates/channel.rss.erb (relative)
|
49
|
+
# to channel.rb.
|
50
|
+
#
|
32
51
|
def to_rss
|
33
52
|
@index_template.result(binding)
|
34
53
|
end
|
35
54
|
|
55
|
+
#
|
56
|
+
# Returns all items (episodes) of this channel, ordered by newest-first.
|
57
|
+
#
|
36
58
|
def items
|
37
59
|
all_items = Array.new
|
38
60
|
@source_files.each{|src|
|
@@ -42,10 +64,9 @@ module Dropcaster
|
|
42
64
|
item.tag.artist = self.author if item.artist.blank?
|
43
65
|
item.image_url = self.image_url if item.image_url.blank?
|
44
66
|
|
45
|
-
#
|
46
|
-
|
47
|
-
item.url
|
48
|
-
item.url += URI.escape(item.file_name, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
67
|
+
# construct absolute URL, based on the channel's enclosure_base attribute
|
68
|
+
enclosure_base << '/' unless enclosure_base =~ /\/$/
|
69
|
+
item.url = URI.join(URI.escape(enclosure_base), URI.escape(item.file_name))
|
49
70
|
|
50
71
|
all_items << item
|
51
72
|
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Dropcaster
|
2
|
+
#
|
3
|
+
# Encapsulates the strategy how to find the channel definition file
|
4
|
+
#
|
5
|
+
class ChannelFileLocator
|
6
|
+
class << self
|
7
|
+
#
|
8
|
+
# Locates the channel definition file based on the <tt>sources</tt> directory.
|
9
|
+
#
|
10
|
+
# * If <tt>sources</tt> is a single file name, the channel definition file is expected
|
11
|
+
# as channel.yml in the same directory.
|
12
|
+
#
|
13
|
+
# * If <tt>sources</tt> is a single directory name, the channel definition file is
|
14
|
+
# expected as channel.yml in that directory.
|
15
|
+
#
|
16
|
+
# * If <tt>sources</tt> is an array of file names, the channel definition file is
|
17
|
+
# expected as channel.yml in the directory common to all files. If the files are
|
18
|
+
# located in more than one directory, an AmbiguousSourcesError is raised. In that
|
19
|
+
# case, the caller should specify the channel.yml as command line parameter.
|
20
|
+
#
|
21
|
+
# * If <tt>sources</tt> is an array with more than a single directory name, an
|
22
|
+
# AmbiguousSourcesError is raised. In that case, the caller should specify the
|
23
|
+
# channel.yml as command line parameter.
|
24
|
+
#
|
25
|
+
def locate(sources)
|
26
|
+
channel_source_dir = nil
|
27
|
+
|
28
|
+
if sources.respond_to?(:at)
|
29
|
+
# More than one source given. Check that they are all in the same directory.
|
30
|
+
distinct_dirs = sources.collect{|source| File.dirname(source)}.uniq
|
31
|
+
|
32
|
+
if 1 == distinct_dirs.size
|
33
|
+
# If all are the in same directory, use that as source directory where channel.yml is expected.
|
34
|
+
channel_source_dir = distinct_dirs.first
|
35
|
+
else
|
36
|
+
# Since no channel_file was specified at the command line, throw and quit
|
37
|
+
raise AmbiguousSourcesError.new(sources)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
# If a single file or directory is given, use that as source directory where channel.yml is expected.
|
41
|
+
channel_source_dir = File.dirname(sources)
|
42
|
+
end
|
43
|
+
|
44
|
+
File.join(channel_source_dir, CHANNEL_YML)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/dropcaster/errors.rb
CHANGED
@@ -1,11 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module Dropcaster
|
2
|
+
class ConfigurationError < StandardError
|
3
|
+
def initialize(msg)
|
4
|
+
super(msg)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class MissingAttributeError < ConfigurationError
|
9
|
+
def initialize(missingAttribute)
|
10
|
+
super("#{missingAttribute} is a mandatory attribute, but it is missing.")
|
11
|
+
end
|
4
12
|
end
|
5
|
-
end
|
6
13
|
|
7
|
-
class
|
8
|
-
|
9
|
-
|
14
|
+
class AmbiguousSourcesError < ConfigurationError
|
15
|
+
def initialize(ambiguousSources)
|
16
|
+
super("The list of sources is ambiguous. Can't derive common directory from these: #{ambiguousSources.inspect}")
|
17
|
+
end
|
10
18
|
end
|
11
19
|
end
|
data/lib/dropcaster/item.rb
CHANGED
@@ -9,7 +9,7 @@ module Dropcaster
|
|
9
9
|
super(Hash.new)
|
10
10
|
|
11
11
|
Mp3Info.open(file_path){|mp3info|
|
12
|
-
self[:file_name] = Pathname.new(file_path).cleanpath.to_s
|
12
|
+
self[:file_name] = Pathname.new(File.expand_path(file_path)).relative_path_from(Pathname.new(Dir.pwd)).cleanpath.to_s
|
13
13
|
self[:tag] = mp3info.tag
|
14
14
|
self[:tag2] = mp3info.tag2
|
15
15
|
self[:duration] = mp3info.length
|
data/lib/dropcaster.rb
CHANGED
@@ -10,7 +10,9 @@ require 'dropcaster/errors'
|
|
10
10
|
require 'dropcaster/hashkeys'
|
11
11
|
require 'dropcaster/channel'
|
12
12
|
require 'dropcaster/item'
|
13
|
+
require 'dropcaster/channel_file_locator'
|
13
14
|
|
14
15
|
module Dropcaster
|
15
16
|
VERSION = File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION]))
|
17
|
+
CHANNEL_YML = 'channel.yml'
|
16
18
|
end
|
data/test/helper.rb
CHANGED
@@ -1,3 +1,10 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'test/unit'
|
3
3
|
require 'dropcaster'
|
4
|
+
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. test unit])
|
6
|
+
|
7
|
+
module DropcasterTest
|
8
|
+
FIXTURES_DIR = File.join(File.dirname(__FILE__), 'fixtures')
|
9
|
+
NS_ITUNES = "itunes:http://www.itunes.com/dtds/podcast-1.0.dtd"
|
10
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'test_channel_xml'
|
3
|
+
require 'xml/libxml'
|
4
|
+
require 'open3'
|
5
|
+
|
6
|
+
#
|
7
|
+
# End-to-end test
|
8
|
+
#
|
9
|
+
# Same as TestChannelXML, but with the XML generated by calling the bin script
|
10
|
+
#
|
11
|
+
class TestApp < TestChannelXML
|
12
|
+
include DropcasterTest
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@options = YAML.load_file(File.join(FIXTURES_DIR, Dropcaster::CHANNEL_YML))
|
16
|
+
@channel = XML::Document.string(%x[bin/dropcaster test/fixtures/iTunes.mp3]).find("//rss/channel").first
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_overwrite_title
|
20
|
+
test_title = 'Alice and Bob in Wonderland'
|
21
|
+
channel = XML::Document.string(%x[bin/dropcaster test/fixtures/iTunes.mp3 --title '#{test_title}']).find("//rss/channel").first
|
22
|
+
assert_equal(test_title, channel.find('title').first.content)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_overwrite_link
|
26
|
+
test_link = 'http://www.example.com/foo/bar'
|
27
|
+
channel = XML::Document.string(%x[bin/dropcaster test/fixtures/iTunes.mp3 --url '#{test_link}']).find("//rss/channel").first
|
28
|
+
assert_equal(test_link, channel.find('link').first.content)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_overwrite_description
|
32
|
+
test_description = 'Testing commandline apps is not that hard.'
|
33
|
+
channel = XML::Document.string(%x[bin/dropcaster test/fixtures/iTunes.mp3 --description '#{test_description}']).find("//rss/channel").first
|
34
|
+
assert_equal(test_description, channel.find('description').first.content)
|
35
|
+
assert_equal(test_description, channel.find('itunes:summary', NS_ITUNES).first.content)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_no_channel_file
|
39
|
+
Open3.popen3('bin/dropcaster'){|stdin, stdout, stderr|
|
40
|
+
assert(stderr.read =~ /No \.\/channel.yml found/)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_overwrite_all
|
45
|
+
test_title = 'Bob and Alice in Wonderland'
|
46
|
+
test_link = 'http://www.example.com/bar/foot'
|
47
|
+
test_description = 'Testing commandline apps is really not that hard.'
|
48
|
+
|
49
|
+
channel = XML::Document.string(%x[bin/dropcaster test/fixtures/iTunes.mp3 --title '#{test_title}' --url '#{test_link}' --description '#{test_description}']).find("//rss/channel").first
|
50
|
+
|
51
|
+
assert_equal(test_title, channel.find('title').first.content)
|
52
|
+
assert_equal(test_link, channel.find('link').first.content)
|
53
|
+
assert_equal(test_description, channel.find('description').first.content)
|
54
|
+
assert_equal(test_description, channel.find('itunes:summary', NS_ITUNES).first.content)
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO --enclosure_base
|
58
|
+
# TODO --channel
|
59
|
+
end
|
data/test/unit/test_channel.rb
CHANGED
@@ -1,43 +1,32 @@
|
|
1
1
|
require 'helper'
|
2
|
-
require 'xml/libxml'
|
3
2
|
|
4
3
|
class TestChannel < Test::Unit::TestCase
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
include DropcasterTest
|
5
|
+
|
8
6
|
def setup
|
9
|
-
@
|
7
|
+
@options = YAML.load_file(File.join(FIXTURES_DIR, Dropcaster::CHANNEL_YML))
|
8
|
+
@channel = Dropcaster::Channel.new(FIXTURES_DIR, @options)
|
10
9
|
end
|
11
|
-
|
12
|
-
def
|
10
|
+
|
11
|
+
def test_item_count
|
13
12
|
assert_equal(1, @channel.items.size)
|
14
|
-
assert_equal('77bf84447c0f69ce4a33a18b0ae1e030b82010de', @channel.items.first.uuid)
|
15
13
|
end
|
14
|
+
|
15
|
+
def test_channel
|
16
|
+
assert_equal(@options[:title], @channel.title)
|
17
|
+
assert_equal(@options[:url], @channel.url)
|
18
|
+
assert_equal(@options[:description], @channel.description)
|
19
|
+
assert_equal(@options[:subtitle], @channel.subtitle)
|
20
|
+
assert_equal(@options[:language], @channel.language)
|
21
|
+
assert_equal(@options[:copyright], @channel.copyright)
|
22
|
+
assert_equal(@options[:author], @channel.author)
|
16
23
|
|
17
|
-
|
18
|
-
|
19
|
-
assert_equal(
|
20
|
-
assert_equal('http://www.example.com/podcast.rss', channel.find('link').first.content)
|
21
|
-
assert_equal('A test channel', channel.find('description').first.content)
|
22
|
-
end
|
24
|
+
owner = @channel.owner
|
25
|
+
assert_equal(@options[:owner][:name], owner[:name])
|
26
|
+
assert_equal(@options[:owner][:email], owner[:email])
|
23
27
|
|
24
|
-
|
25
|
-
options = YAML.load_file(File.join(FIXTURES_DIR, 'test_channel.yml'))
|
26
|
-
channel = XML::Document.string(Dropcaster::Channel.new(FIXTURES_DIR, options).to_rss).find("//rss/channel").first
|
27
|
-
assert_equal(options[:title], channel.find('title').first.content)
|
28
|
-
assert_equal(options[:url], channel.find('link').first.content)
|
29
|
-
assert_equal(options[:description], channel.find('description').first.content)
|
30
|
-
assert_equal(options[:subtitle], channel.find('itunes:subtitle', NS_ITUNES).first.content)
|
31
|
-
assert_equal(options[:language], channel.find('language').first.content)
|
32
|
-
assert_equal(options[:copyright], channel.find('copyright').first.content)
|
33
|
-
assert_equal(options[:author], channel.find('itunes:author', NS_ITUNES).first.content)
|
34
|
-
|
35
|
-
owner = channel.find('itunes:owner', NS_ITUNES).first
|
36
|
-
assert_equal(options[:owner][:name], owner.find('itunes:name', NS_ITUNES).first.content)
|
37
|
-
assert_equal(options[:owner][:email], owner.find('itunes:email', NS_ITUNES).first.content)
|
38
|
-
|
39
|
-
assert_equal(options[:image_url], channel.find('itunes:image', NS_ITUNES).first['href'])
|
28
|
+
assert_equal(@options[:image_url], @channel.image_url)
|
40
29
|
# TODO :categories: ['Technology', 'Gadgets']
|
41
|
-
assert_equal(options[:explicit], channel.
|
30
|
+
assert_equal(@options[:explicit], @channel.explicit)
|
42
31
|
end
|
43
32
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
class TestChannelLocator < Test::Unit::TestCase
|
5
|
+
include DropcasterTest
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :temp_dir
|
9
|
+
|
10
|
+
def startup
|
11
|
+
@temp_dir = Dir.mktmpdir
|
12
|
+
end
|
13
|
+
|
14
|
+
def shutdown
|
15
|
+
FileUtils.remove_entry_secure(@temp_dir)
|
16
|
+
end
|
17
|
+
|
18
|
+
def suite
|
19
|
+
# from http://stackoverflow.com/questions/255969#778701
|
20
|
+
_suite = super
|
21
|
+
|
22
|
+
def _suite.run(*args)
|
23
|
+
TestChannelLocator.startup()
|
24
|
+
super
|
25
|
+
TestChannelLocator.shutdown()
|
26
|
+
end
|
27
|
+
|
28
|
+
_suite
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_single_file
|
33
|
+
sources = File.join(TestChannelLocator.temp_dir, 'single_file.mp3')
|
34
|
+
assert_location(sources)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_current_directory
|
38
|
+
sources = File.join(TestChannelLocator.temp_dir, '.')
|
39
|
+
assert_location(sources)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_single_directory
|
43
|
+
sources = File.join(TestChannelLocator.temp_dir, 'single_dir')
|
44
|
+
assert_location(sources)
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_array_of_files_same_dir
|
48
|
+
sources = Array.new
|
49
|
+
sources << File.join(TestChannelLocator.temp_dir, 'file1.mp3')
|
50
|
+
sources << File.join(TestChannelLocator.temp_dir, 'file2.mp3')
|
51
|
+
sources << File.join(TestChannelLocator.temp_dir, 'file3.mp3')
|
52
|
+
|
53
|
+
assert_location(sources)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_array_of_files_different_dir
|
57
|
+
sources = Array.new
|
58
|
+
sources << File.join(TestChannelLocator.temp_dir, 'foo', 'file1.mp3')
|
59
|
+
sources << File.join(TestChannelLocator.temp_dir, 'bar', 'file1.mp3')
|
60
|
+
sources << File.join(TestChannelLocator.temp_dir, 'baz', 'file1.mp3')
|
61
|
+
|
62
|
+
assert_raises Dropcaster::AmbiguousSourcesError do
|
63
|
+
Dropcaster::ChannelFileLocator.locate(sources)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_array_with_one_directory
|
68
|
+
assert_location(File.join(TestChannelLocator.temp_dir, ['single_dir']))
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_array_with_more_than_a_single_directory
|
72
|
+
Dir.mktmpdir{|tmp_dir1|
|
73
|
+
Dir.mktmpdir{|tmp_dir2|
|
74
|
+
sources = Array.new
|
75
|
+
sources << File.join(tmp_dir1, 'another_dir')
|
76
|
+
sources << File.join(tmp_dir2, 'another_dir')
|
77
|
+
|
78
|
+
assert_raises Dropcaster::AmbiguousSourcesError do
|
79
|
+
Dropcaster::ChannelFileLocator.locate(sources)
|
80
|
+
end
|
81
|
+
}
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def assert_location(sources)
|
88
|
+
channel_file = Dropcaster::ChannelFileLocator.locate(sources)
|
89
|
+
assert_equal(File.join(TestChannelLocator.temp_dir, Dropcaster::CHANNEL_YML), channel_file)
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'xml/libxml'
|
3
|
+
|
4
|
+
class TestChannelXML < Test::Unit::TestCase
|
5
|
+
include DropcasterTest
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@options = YAML.load_file(File.join(FIXTURES_DIR, Dropcaster::CHANNEL_YML))
|
9
|
+
@channel = XML::Document.string(Dropcaster::Channel.new(FIXTURES_DIR, @options).to_rss).find("//rss/channel").first
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_item
|
13
|
+
item = @channel.find("item").first
|
14
|
+
assert(item)
|
15
|
+
|
16
|
+
assert_equal('iTunes Name', item.find('title').first.content)
|
17
|
+
assert_equal('iTunes Artist', item.find('itunes:author', NS_ITUNES).first.content)
|
18
|
+
assert_equal('iTunes Description (Video Pane)', item.find('itunes:summary', NS_ITUNES).first.content)
|
19
|
+
assert_equal('http://example.com/podcasts/everything/AllAboutEverything.jpg', item.find('itunes:image', NS_ITUNES).first['href'])
|
20
|
+
|
21
|
+
enclosure = item.find('enclosure').first
|
22
|
+
assert(enclosure)
|
23
|
+
assert_equal('http://www.example.com/podcasts/everything/test/fixtures/iTunes.mp3', enclosure['url'])
|
24
|
+
assert_equal('58119', enclosure['length'])
|
25
|
+
assert_equal('audio/mp3', enclosure['type'])
|
26
|
+
|
27
|
+
guid = item.find('guid').first
|
28
|
+
assert(guid)
|
29
|
+
assert_equal('false', guid['isPermaLink'])
|
30
|
+
assert_equal('77bf84447c0f69ce4a33a18b0ae1e030b82010de', guid.content)
|
31
|
+
|
32
|
+
assert_equal('Sat, 01 Oct 2011 14:08:20 +0200', item.find('pubDate').first.content)
|
33
|
+
assert_equal('3', item.find('itunes:duration', NS_ITUNES).first.content)
|
34
|
+
|
35
|
+
# Not used in our fixture yet
|
36
|
+
# assert_equal('', item.find('itunes:subtitle', NS_ITUNES).first.content)
|
37
|
+
# assert_equal('', item.find('itunes:keywords', NS_ITUNES).first.content)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_attributes_mandatory
|
41
|
+
options = {:title => 'Test Channel',
|
42
|
+
:url => 'http://www.example.com/',
|
43
|
+
:description => 'A test channel',
|
44
|
+
:enclosure_base => 'http://www.example.com/foo/bar',
|
45
|
+
}
|
46
|
+
@channel = XML::Document.string(Dropcaster::Channel.new(FIXTURES_DIR, options).to_rss).find("//rss/channel").first
|
47
|
+
assert_equal('Test Channel', @channel.find('title').first.content)
|
48
|
+
assert_equal('http://www.example.com/', @channel.find('link').first.content)
|
49
|
+
assert_equal('A test channel', @channel.find('description').first.content)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_attributes_complete
|
53
|
+
assert_equal(@options[:title], @channel.find('title').first.content)
|
54
|
+
assert_equal(@options[:url], @channel.find('link').first.content)
|
55
|
+
assert_equal(@options[:description], @channel.find('description').first.content)
|
56
|
+
assert_equal(@options[:subtitle], @channel.find('itunes:subtitle', NS_ITUNES).first.content)
|
57
|
+
assert_equal(@options[:language], @channel.find('language').first.content)
|
58
|
+
assert_equal(@options[:copyright], @channel.find('copyright').first.content)
|
59
|
+
assert_equal(@options[:author], @channel.find('itunes:author', NS_ITUNES).first.content)
|
60
|
+
|
61
|
+
owner = @channel.find('itunes:owner', NS_ITUNES).first
|
62
|
+
assert_equal(@options[:owner][:name], owner.find('itunes:name', NS_ITUNES).first.content)
|
63
|
+
assert_equal(@options[:owner][:email], owner.find('itunes:email', NS_ITUNES).first.content)
|
64
|
+
|
65
|
+
assert_equal(@options[:image_url], @channel.find('itunes:image', NS_ITUNES).first['href'])
|
66
|
+
# TODO :categories: ['Technology', 'Gadgets']
|
67
|
+
assert_equal(@options[:explicit], @channel.find('itunes:explicit', NS_ITUNES).first.content)
|
68
|
+
end
|
69
|
+
end
|
data/test/unit/test_item.rb
CHANGED
@@ -10,7 +10,7 @@ class TestItem < Test::Unit::TestCase
|
|
10
10
|
assert_equal(58119, @item.file_size)
|
11
11
|
assert_equal('77bf84447c0f69ce4a33a18b0ae1e030b82010de', @item.uuid)
|
12
12
|
assert_equal(1317470900, @item.pub_date.to_i)
|
13
|
-
assert_equal(
|
13
|
+
assert_equal('test/fixtures/iTunes.mp3', @item.file_name)
|
14
14
|
end
|
15
15
|
|
16
16
|
def test_tag
|
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: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- nerab
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-10-
|
18
|
+
date: 2011-10-05 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
requirement: &id001 !ruby/object:Gem::Requirement
|
@@ -91,6 +91,20 @@ dependencies:
|
|
91
91
|
name: libxml-ruby
|
92
92
|
prerelease: false
|
93
93
|
type: :development
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
hash: 3
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: "0"
|
104
|
+
version_requirements: *id006
|
105
|
+
name: rdoc
|
106
|
+
prerelease: false
|
107
|
+
type: :development
|
94
108
|
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.
|
95
109
|
email: nerab@gmx.at
|
96
110
|
executables:
|
@@ -113,19 +127,26 @@ files:
|
|
113
127
|
- VERSION
|
114
128
|
- bin/dropcaster
|
115
129
|
- bin/lstags
|
130
|
+
- doc/infoPane.png
|
131
|
+
- doc/lyricsPane.png
|
132
|
+
- doc/sample-channel.yml
|
133
|
+
- doc/sample-sidecar.yml
|
134
|
+
- doc/videoPane.png
|
135
|
+
- dropcaster.gemspec
|
116
136
|
- lib/dropcaster.rb
|
117
137
|
- lib/dropcaster/channel.rb
|
138
|
+
- lib/dropcaster/channel_file_locator.rb
|
118
139
|
- lib/dropcaster/errors.rb
|
119
140
|
- lib/dropcaster/hashkeys.rb
|
120
141
|
- lib/dropcaster/item.rb
|
121
142
|
- templates/channel.rss.erb
|
143
|
+
- test/fixtures/channel.yml
|
122
144
|
- test/fixtures/iTunes.mp3
|
123
|
-
- test/fixtures/infoPane.png
|
124
|
-
- test/fixtures/lyricsPane.png
|
125
|
-
- test/fixtures/test_channel.yml
|
126
|
-
- test/fixtures/videoPane.png
|
127
145
|
- test/helper.rb
|
146
|
+
- test/unit/test_app.rb
|
128
147
|
- test/unit/test_channel.rb
|
148
|
+
- test/unit/test_channel_locator.rb
|
149
|
+
- test/unit/test_channel_xml.rb
|
129
150
|
- test/unit/test_item.rb
|
130
151
|
homepage: http://github.com/nerab/dropcaster
|
131
152
|
licenses:
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|