tapas 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 99329bb68ac39ab9bce498a9bac1703d5712ff20
4
+ data.tar.gz: de4f0315fa189e111dc917d0563ced75951e542a
5
+ SHA512:
6
+ metadata.gz: 2fb7e87468b291aacac6cded2434ad242250dd3d841440e6a0ae67ccb59603e6aa9662a2778f4d8718806157279aa5739c3f5e0d82ff0bd4a3a1627e3217c863
7
+ data.tar.gz: 32a919b80686a2255ce6f220e3f2e9c3bd352772a1185e22f9762756db0f291dc7682906fc9c17d7260d2a90be1bcde7f13bad8354b6207ef17c32b82a152688
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'mocha', require: false
4
+
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jon Riddle
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Tapas
2
+
3
+ Tapas downloads the latest RubyTapas episode, episodes by number, and episodes matching a given search.
4
+
5
+ ## Installation
6
+
7
+ $ gem install tapas
8
+
9
+ ## Usage
10
+
11
+ tapas # Download the latest episode
12
+
13
+ tapas -e 100 # Download episode 100
14
+
15
+ tapas -e 100-105 # Download episodes 100 through 105
16
+
17
+ tapas -t foo # Download episodes with title containing 'foo'
18
+
19
+ tapas -l -t foo # List (don't download) episodes with title containing 'foo'
20
+
21
+ tapas -h # Show options
22
+
23
+
24
+ ## Configuration Options
25
+
26
+ The default configuration file is `~/.tapas` and is of the format:
27
+
28
+ setting: value
29
+ setting: value
30
+
31
+ The following options are supported (default value):
32
+
33
+ - username
34
+ - password
35
+ - feed_url (https://rubytapas.dpdcart.com/feed)
36
+ - feed_file # local feed - will override feed_url if set
37
+ - destination (.)
38
+
39
+ ## Contributing
40
+
41
+ 1. Fork it
42
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
43
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
44
+ 4. Push to the branch (`git push origin my-new-feature`)
45
+ 5. Create new Pull Request
46
+
47
+ © 2013 jdrNetworking, LLC
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ end
7
+
8
+ task :default => :test
data/bin/tapas ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'tapas'
5
+ rescue NameError
6
+ require 'rubygems'
7
+ require 'tapas'
8
+ end
9
+
10
+ require 'optparse'
11
+
12
+ options = { last: true, episodes: [] }
13
+ OptionParser.new do |opts|
14
+ opts.banner = "Usage: #{File.basename($0)} [-l] [-n episode[-episode] ] [-t title] [-d description] [-s search]"
15
+
16
+ opts.separator ''
17
+
18
+ opts.on('-l', '--list-only', 'List episode(s) to be downloaded without downloading them') do
19
+ options[:list_only] = true
20
+ end
21
+
22
+ opts.on('-a', '--all', 'Select all episodes') do |all|
23
+ options.delete(:last)
24
+ options[:all] = true
25
+ end
26
+
27
+ opts.on('-n', '--episode N[-M]', 'Select episode(s) N [through M]') do |episode|
28
+ options.delete(:last)
29
+ if episode.include?('-')
30
+ first, last = episode.split('-').map(&:to_i)
31
+ options[:episodes] += (first..last).to_a
32
+ else
33
+ options[:episodes] << episode.to_i
34
+ end
35
+ end
36
+
37
+ opts.on('-t', '--title TERM', 'Select episodes with TERM in the title') do |term|
38
+ options.delete(:last)
39
+ options[:title] = term
40
+ end
41
+
42
+ opts.on('-d', '--description TERM', 'Select episodes with TERM in the description') do |term|
43
+ options.delete(:last)
44
+ options[:description] = term
45
+ end
46
+
47
+ opts.on('-s', '--search TERM', 'Select episodes with TERM in the title or description') do |term|
48
+ options.delete(:last)
49
+ options[:search] = term
50
+ end
51
+
52
+ opts.on('-q', '--quiet', 'Output nothing except errors') do |quiet|
53
+ options[:quiet] = quiet
54
+ end
55
+
56
+ opts.on('--feed-file FILE', 'Use local file instead of downloading feed') do |file|
57
+ options[:feed_file] = file
58
+ end
59
+
60
+ opts.separator ''
61
+ opts.separator 'With no arguments, downloads the last episode'
62
+ opts.separator ''
63
+
64
+ opts.on_tail('-c', '--config', 'Show configuration options') do
65
+ options[:show_config] = true
66
+ end
67
+
68
+ opts.on_tail('-h', '--help', 'Show this message') do
69
+ puts opts
70
+ exit 1
71
+ end
72
+
73
+ opts.on_tail('--version') do
74
+ puts "Tapas #{Tapas::VERSION}"
75
+ exit 0
76
+ end
77
+ end.parse!
78
+
79
+ config = Tapas::Config.new(options)
80
+
81
+ if config.show_config
82
+ config.delete_setting :show_config
83
+ puts config
84
+ exit
85
+ end
86
+
87
+ begin
88
+ feed_source = Tapas::FeedSource.new(config)
89
+ collection = Tapas::EpisodeCollection.new(feed_source)
90
+ filter = Tapas::EpisodeFilter.new(collection, config)
91
+
92
+ filter.episodes.each do |episode|
93
+ puts " #{episode}" unless config.quiet
94
+ next if config.list_only
95
+ Tapas::Downloader.new(episode, config).download
96
+ end
97
+ rescue Tapas::UnauthorizedError
98
+ puts "Uh oh! It looks like the feed requires authentication."
99
+ if config.username && config.password
100
+ puts "Your username and/or password were not accepted."
101
+ else
102
+ puts "No username or password were supplied. Set them in #{config.default_path}"
103
+ end
104
+ end
data/lib/tapas.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative "tapas/exceptions"
2
+ require_relative "tapas/config"
3
+ require_relative "tapas/feed_source"
4
+ require_relative "tapas/episode"
5
+ require_relative "tapas/episode_collection"
6
+ require_relative "tapas/episode_filter"
7
+ require_relative "tapas/downloader"
@@ -0,0 +1,70 @@
1
+ require 'yaml'
2
+ require 'ostruct'
3
+
4
+ module Tapas
5
+ class Config
6
+ attr_accessor :path
7
+ attr_reader :config
8
+
9
+ def initialize(args = {})
10
+ @path = ::Pathname.new(args.delete(:path) || default_path)
11
+ @config = default_config
12
+ load_config(path.expand_path)
13
+ args.each { |k,v| config[k] = v }
14
+ end
15
+
16
+ def default_path
17
+ '~/.tapas'
18
+ end
19
+
20
+ def delete_setting(setting)
21
+ config.delete_field setting
22
+ end
23
+
24
+ def method_missing(name, *args)
25
+ if name.to_s.end_with?('=')
26
+ key = name.to_s.gsub(/=$/, '')
27
+ config[key] = args.first
28
+ else
29
+ config[name]
30
+ end
31
+ end
32
+
33
+ def respond_to_missing?(name)
34
+ true
35
+ end
36
+
37
+ def to_s
38
+ max_length = config.to_h.keys.map(&:length).max
39
+ config.each_pair.map do |setting, value|
40
+ "%-#{max_length}s = %s" % format_setting_value(setting, value)
41
+ end.join("\n")
42
+ end
43
+
44
+ private
45
+
46
+ def default_config
47
+ ::OpenStruct.new.tap do |config|
48
+ config.feed_url = 'https://rubytapas.dpdcart.com/feed'
49
+ config.destination = '.'
50
+ end
51
+ end
52
+
53
+ def load_config(path)
54
+ return unless path.exist?
55
+ ::YAML.load_file(path).each do |setting, value|
56
+ config[setting] = value
57
+ end
58
+ end
59
+
60
+ def format_setting_value(setting, value)
61
+ return [ setting, value ? '✓' : '✕' ] if [true, false].include?(value)
62
+
63
+ case setting
64
+ when :destination then [ setting, ::File.expand_path(value) ]
65
+ when :password then [ setting, value[0,1] + '*' * (value.length - 2) + value[-1,1] ]
66
+ else [ setting, value ]
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+ require 'open-uri'
2
+ require 'ruby-progressbar'
3
+
4
+ module Tapas
5
+ class Downloader
6
+ attr_reader :episode, :config, :output, :progress_bar
7
+
8
+ def initialize(episode, config)
9
+ @episode = episode
10
+ @config = config
11
+ @output = default_output(episode, config)
12
+ end
13
+
14
+ def download
15
+ open_opts = {
16
+ http_basic_authentication: [config.username, config.password],
17
+ content_length_proc: length_proc,
18
+ progress_proc: progress_proc
19
+ }
20
+ open(episode.url, open_opts) do |download|
21
+ File.open(output, 'wb') do |out|
22
+ out.write download.read
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def default_output(episode, config)
30
+ filename = "RubyTapas - %3d - %s%s" % [ episode.number, episode.title, File.extname(episode.url) ]
31
+ File.expand_path(File.join(config.destination, filename))
32
+ end
33
+
34
+ def length_proc
35
+ ->(total) {
36
+ if total && total > 0
37
+ @progress_bar = ProgressBar.create total: total
38
+ end
39
+ }
40
+ end
41
+
42
+ def progress_proc
43
+ ->(size) {
44
+ update_progress(progress_bar, size)
45
+ }
46
+ end
47
+
48
+ def update_progress(progress_bar, size)
49
+ return unless progress_bar
50
+
51
+ progress_bar.total = nil if progress_bar.total && size > progress_bar.total
52
+
53
+ if progress_bar.total
54
+ progress_bar.progress = size
55
+ else
56
+ progress_bar.increment
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module Tapas
2
+ class Episode < Struct.new(:number, :title, :description, :url)
3
+ include Comparable
4
+
5
+ def title_matches?(text)
6
+ title.downcase.include?(text.downcase)
7
+ end
8
+
9
+ def description_matches?(text)
10
+ description.downcase.include?(text.downcase)
11
+ end
12
+
13
+ def <=>(other)
14
+ [number.to_i, title] <=> [other.number.to_i, other.title]
15
+ end
16
+
17
+ def to_s
18
+ if number
19
+ "%3d - %s" % [ number, title ]
20
+ else
21
+ " %s" % title
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ require 'nokogiri'
2
+ require 'htmlentities'
3
+
4
+ module Tapas
5
+ class EpisodeCollection
6
+ attr_reader :doc, :episodes
7
+
8
+ def initialize(feed_source)
9
+ @doc = Nokogiri.parse(feed_source.feed)
10
+ @episodes = parse(doc)
11
+ end
12
+
13
+ private
14
+
15
+ def parse(doc)
16
+ doc.css('item').select do |item|
17
+ episode?(item)
18
+ end.map do |item|
19
+ number = item_number(item)
20
+ title = item_title(item)
21
+ url = item_url(item)
22
+ description = item_description(item)
23
+ Episode.new(number, title, description, url)
24
+ end
25
+ end
26
+
27
+ def item_number(item)
28
+ number = item.at(:title).text[/^\d+/]
29
+ number &&= number.to_i
30
+ end
31
+
32
+ def item_title(item)
33
+ title = HTMLEntities.new.decode(item.at(:title).text[/^\d*\s*:?\s*(.*)/, 1])
34
+ end
35
+
36
+ def item_url(item)
37
+ item.at(:enclosure)[:url]
38
+ end
39
+
40
+ def item_description(item)
41
+ text = item.at(:description).text
42
+ doc = Nokogiri.parse(text)
43
+ doc.at('p').text
44
+ end
45
+
46
+ def episode?(item)
47
+ !!item.at(:enclosure)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ module Tapas
2
+ class EpisodeFilter
3
+ attr_reader :collection, :config
4
+
5
+ def initialize(collection, config)
6
+ @collection = collection
7
+ @config = config
8
+ end
9
+
10
+ def episodes
11
+ return [collection.episodes.sort.last] if config.last
12
+ return collection.episodes.sort if config.all
13
+
14
+ [
15
+ by_number(config.episodes),
16
+ title(config.title),
17
+ description(config.description),
18
+ search(config.search),
19
+ ].flatten.uniq.sort
20
+ end
21
+
22
+ private
23
+
24
+ def by_number(episode_numbers)
25
+ return [] if episode_numbers.empty?
26
+
27
+ episode_numbers.map do |episode_number|
28
+ number(episode_number)
29
+ end
30
+ end
31
+
32
+ def number(number)
33
+ collection.episodes.detect { |episode| episode.number == number }
34
+ end
35
+
36
+ def title(text)
37
+ return [] unless text
38
+ collection.episodes.select { |episode| episode.title_matches?(text) }
39
+ end
40
+
41
+ def description(text)
42
+ return [] unless text
43
+ collection.episodes.select { |episode| episode.description_matches?(text) }
44
+ end
45
+
46
+ def search(text)
47
+ return [] unless text
48
+ [title(text), description(text)].flatten.uniq
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module Tapas
2
+ UnauthorizedError = Class.new(RuntimeError)
3
+ end
@@ -0,0 +1,22 @@
1
+ require 'open-uri'
2
+
3
+ module Tapas
4
+ class FeedSource
5
+ attr_reader :config
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def feed
12
+ if config.feed_file
13
+ File.read(config.feed_file)
14
+ elsif config.feed_url
15
+ open(config.feed_url, http_basic_authentication: [config.username, config.password]).read
16
+ end
17
+ rescue OpenURI::HTTPError => e
18
+ raise Tapas::UnauthorizedError if e.message == "401 Unauthorized"
19
+ raise
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Tapas
2
+ VERSION = "0.1.2"
3
+ end
data/tapas.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tapas/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tapas"
8
+ spec.version = Tapas::VERSION
9
+ spec.authors = ["Jon Riddle"]
10
+ spec.email = ["jon@jdrnetworking.com"]
11
+ spec.description = %q{Download RubyTapas episodes}
12
+ spec.summary = %q{tapas downloads the latest RubyTapas episode or episodes matching a given search term}
13
+ spec.homepage = "https://github.com/jdrnetworking/tapas"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "nokogiri", "~> 1.6"
22
+ spec.add_runtime_dependency "ruby-progressbar", "~> 1.2"
23
+ spec.add_runtime_dependency "htmlentities", "~> 4.3"
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "activesupport", "~> 3.2.14"
28
+ spec.add_development_dependency "minitest", "4.7.5"
29
+ spec.add_development_dependency "mocha"
30
+ spec.add_development_dependency "pry"
31
+ spec.add_development_dependency "pry-debugger"
32
+ end
data/test/feed.xml ADDED
@@ -0,0 +1,107 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
3
+ <channel>
4
+ <title>RubyTapas</title>
5
+ <link>https://rubytapas.dpdcart.com/subscriber/content</link>
6
+ <description/>
7
+ <pubDate>Thu, 17 Oct 2013 19:14:00 -0400</pubDate>
8
+ <managingEditor>contact@shiprise.net (Avdi Grimm)</managingEditor>
9
+ <language>en</language>
10
+ <copyright>Copyright 2013 RubyTapas</copyright>
11
+ <generator>getdpd.com</generator>
12
+ <itunes:summary>RubyTapas: Small plates of gourmet code.</itunes:summary>
13
+ <itunes:image href="https://getdpd.com/uploads/ruby-tapas.png"/>
14
+ <image>
15
+ <url>https://getdpd.com/uploads/ruby-tapas.png</url>
16
+ <title>Ruby Tapas</title>
17
+ <link>https://rubytapas.dpdcart.com/subscriber/content</link>
18
+ <width>849</width>
19
+ <height>849</height>
20
+ </image>
21
+ <item>
22
+ <title><![CDATA[005 Array Literals]]></title>
23
+ <link>https://rubytapas.dpdcart.com/subscriber/post?id=27</link>
24
+ <description><![CDATA[<div class="blog-entry">
25
+ <div class="blog-content"><p>Composing command lines with fancy array literals.</p>
26
+ </div>
27
+ <h3>Attached Files</h3>
28
+ <ul>
29
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=38">RubyTapas005.mp4</a></li>
30
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=39">005-array-literals.html</a></li>
31
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=40">005-array-literals.rb</a></li>
32
+ </ul></div>]]></description>
33
+ <guid isPermaLink="false">dpd-fc854d43484d212cf08a36fd13e5cf8c7fa427d5</guid>
34
+ <pubDate>Wed, 03 Oct 2012 09:00:00 -0400</pubDate>
35
+ <enclosure url="https://rubytapas.dpdcart.com/feed/download/38/RubyTapas005.mp4" length="28831935" type="video/mp4"/>
36
+ <itunes:image href="https://getdpd.com/uploads/ruby-tapas.png"/>
37
+ </item>
38
+ <item>
39
+ <title><![CDATA[004 Barewords]]></title>
40
+ <link>https://rubytapas.dpdcart.com/subscriber/post?id=26</link>
41
+ <description><![CDATA[<div class="blog-entry">
42
+ <div class="blog-content"><p>In this longer-than-usual episode, some thoughts on how to enable method logic to remain stable and unchanged while evolving and changing the source and scope of the values used by the logic.</p>
43
+ </div>
44
+ <h3>Attached Files</h3>
45
+ <ul>
46
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=43">barewords.rb</a></li>
47
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=44">barewords.html</a></li>
48
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=45">RubyTapas-Barewords.mp4</a></li>
49
+ </ul></div>]]></description>
50
+ <guid isPermaLink="false">dpd-79af0936dda4a452fa19be8b54aecd2cb1885e2f</guid>
51
+ <pubDate>Mon, 01 Oct 2012 09:00:00 -0400</pubDate>
52
+ <enclosure url="https://rubytapas.dpdcart.com/feed/download/45/RubyTapas-Barewords.mp4" length="106907561" type="video/mp4"/>
53
+ <itunes:image href="https://getdpd.com/uploads/ruby-tapas.png"/>
54
+ </item>
55
+ <item>
56
+ <title><![CDATA[003 Character Literals]]></title>
57
+ <link>https://rubytapas.dpdcart.com/subscriber/post?id=21</link>
58
+ <description><![CDATA[<div class="blog-entry">
59
+ <div class="blog-content"><p>Character literal syntax in Ruby.</p>
60
+ </div>
61
+ <h3>Attached Files</h3>
62
+ <ul>
63
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=32">RubyTapas003.mp4</a></li>
64
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=33">003-char-literals.html</a></li>
65
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=34">003-char-literals.rb</a></li>
66
+ </ul></div>]]></description>
67
+ <guid isPermaLink="false">dpd-ed1f01e1872210288982a61742c81df4c7905a4b</guid>
68
+ <pubDate>Fri, 28 Sep 2012 09:00:00 -0400</pubDate>
69
+ <enclosure url="https://rubytapas.dpdcart.com/feed/download/32/RubyTapas003.mp4" length="6910954" type="video/mp4"/>
70
+ <itunes:image href="https://getdpd.com/uploads/ruby-tapas.png"/>
71
+ </item>
72
+ <item>
73
+ <title><![CDATA[002 Large Integer&lt;Literals]]></title>
74
+ <link>https://rubytapas.dpdcart.com/subscriber/post?id=20</link>
75
+ <description><![CDATA[<div class="blog-entry">
76
+ <div class="blog-content"><p>How to format big numbers&lt;so that they are readable.</p>
77
+ </div>
78
+ <h3>Attached Files</h3>
79
+ <ul>
80
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=29">RubyTapas002.mp4</a></li>
81
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=30">002-large-integer-literals.html</a></li>
82
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=31">002-large-integer-literals.rb</a></li>
83
+ </ul></div>]]></description>
84
+ <guid isPermaLink="false">dpd-8652bee88eb6fad318e874a29a26513083d47bab</guid>
85
+ <pubDate>Wed, 26 Sep 2012 09:00:00 -0400</pubDate>
86
+ <enclosure url="https://rubytapas.dpdcart.com/feed/download/29/RubyTapas002.mp4" length="4258184" type="video/mp4"/>
87
+ <itunes:image href="https://getdpd.com/uploads/ruby-tapas.png"/>
88
+ </item>
89
+ <item>
90
+ <title><![CDATA[001 Binary Literals]]></title>
91
+ <link>https://rubytapas.dpdcart.com/subscriber/post?id=18</link>
92
+ <description><![CDATA[<div class="blog-entry">
93
+ <div class="blog-content"><p>In this inaugural episode, a look at a handy syntax for writing out binary numbers.</p>
94
+ </div>
95
+ <h3>Attached Files</h3>
96
+ <ul>
97
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=25">RubyTapas001.mp4</a></li>
98
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=26">001-binary-literals.html</a></li>
99
+ <li><a href="https://rubytapas.dpdcart.com/subscriber/download?file_id=27">001-binary-literals.rb</a></li>
100
+ </ul></div>]]></description>
101
+ <guid isPermaLink="false">dpd-89e8004c8242e7ad548833bef1e18a5b575c92c1</guid>
102
+ <pubDate>Mon, 24 Sep 2012 09:00:00 -0400</pubDate>
103
+ <enclosure url="https://rubytapas.dpdcart.com/feed/download/25/RubyTapas001.mp4" length="12502397" type="video/mp4"/>
104
+ <itunes:image href="https://getdpd.com/uploads/ruby-tapas.png"/>
105
+ </item>
106
+ </channel>
107
+ </rss>
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'tapas'
3
+ require 'pry'
4
+ require 'active_support/test_case'
5
+ require 'mocha/setup'
6
+ require 'minitest/autorun'
@@ -0,0 +1,58 @@
1
+ require 'minitest_helper'
2
+ require 'tempfile'
3
+
4
+ class TestConfig < ActiveSupport::TestCase
5
+ test 'should have default config path' do
6
+ config = Tapas::Config.new
7
+ refute_nil config.path
8
+ end
9
+
10
+ test 'should override config path' do
11
+ config = Tapas::Config.new(path: '~/.foo')
12
+ assert_equal '~/.foo', config.path.to_s
13
+ end
14
+
15
+ test 'should load configuration from config file' do
16
+ Tempfile.open('tapas') do |tempfile|
17
+ tempfile << <<-EOT
18
+ username: foo
19
+ password: bar
20
+ EOT
21
+ tempfile.close
22
+ config = Tapas::Config.new(path: tempfile.path)
23
+ assert_equal 'foo', config.username
24
+ assert_equal 'bar', config.password
25
+ end
26
+ end
27
+
28
+ test 'should override config file' do
29
+ Tempfile.open('tapas') do |tempfile|
30
+ tempfile << <<-EOT
31
+ username: foo
32
+ password: bar
33
+ EOT
34
+ tempfile.close
35
+ config = Tapas::Config.new(path: tempfile.path, username: 'baz')
36
+ assert_equal 'baz', config.username
37
+ assert_equal 'bar', config.password
38
+ end
39
+ end
40
+
41
+ test 'should return nil for unknown options' do
42
+ config = Tapas::Config.new
43
+ assert_nil config.foobar
44
+ end
45
+
46
+ test 'should set option' do
47
+ config = Tapas::Config.new
48
+ config.foobar = 'baz'
49
+ assert_equal 'baz', config.foobar
50
+ end
51
+
52
+ test 'should delete option' do
53
+ config = Tapas::Config.new foo: 'bar'
54
+ assert_equal 'bar', config.foo
55
+ config.delete_setting :foo
56
+ assert_nil config.foo
57
+ end
58
+ end
@@ -0,0 +1,46 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestEpisode < ActiveSupport::TestCase
4
+ setup do
5
+ @episode = Tapas::Episode.new(
6
+ 5,
7
+ 'Array Literals',
8
+ 'Composing command lines with fancy array literals.',
9
+ 'https://rubytapas.dpdcart.com/feed/download/38/RubyTapas005.mp4'
10
+ )
11
+ end
12
+
13
+ test 'should match title' do
14
+ assert @episode.title_matches?('Array')
15
+ end
16
+
17
+ test 'should match title without case sensitivity' do
18
+ assert @episode.title_matches?('array')
19
+ end
20
+
21
+ test 'should match description' do
22
+ assert @episode.description_matches?('Composing')
23
+ end
24
+
25
+ test 'should match description without case sensitivity' do
26
+ assert @episode.description_matches?('composing')
27
+ end
28
+
29
+ test 'should be sort by episode number first' do
30
+ episode_2 = Tapas::Episode.new(4, 'Array Literals', '', '')
31
+ sorted = [@episode, episode_2].sort
32
+ assert_equal [episode_2, @episode], sorted
33
+ end
34
+
35
+ test 'should sort by title next' do
36
+ episode_2 = Tapas::Episode.new(5, 'Binary Literals', '', '')
37
+ sorted = [episode_2, @episode].sort
38
+ assert_equal [@episode, episode_2], sorted
39
+ end
40
+
41
+ test 'should sort without episode number' do
42
+ episode_2 = Tapas::Episode.new(nil, 'Binary Literals', '', '')
43
+ sorted = [@episode, episode_2].sort
44
+ assert_equal [episode_2, @episode], sorted
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestEpisodeCollection < ActiveSupport::TestCase
4
+ setup do
5
+ @collection = Tapas::EpisodeCollection.new(feed_source)
6
+ end
7
+
8
+ test 'should generate episode list' do
9
+ assert_equal 5, @collection.episodes.size
10
+ end
11
+
12
+ test 'should set episode attributes' do
13
+ episode = @collection.episodes.first
14
+ assert_equal 'Array Literals', episode.title
15
+ assert_equal 5, episode.number
16
+ assert_equal 'Composing command lines with fancy array literals.', episode.description
17
+ assert_equal 'https://rubytapas.dpdcart.com/feed/download/38/RubyTapas005.mp4', episode.url
18
+ end
19
+
20
+ test 'should decode HTML entities in title' do
21
+ episode = @collection.episodes.detect { |e| e.number == 2 }
22
+ assert_equal 'Large Integer<Literals', episode.title
23
+ end
24
+
25
+ test 'should decode HTML entities in description' do
26
+ episode = @collection.episodes.detect { |e| e.number == 2 }
27
+ assert_equal 'How to format big numbers<so that they are readable.', episode.description
28
+ end
29
+
30
+ def feed_source
31
+ stub feed: File.read(File.expand_path('../feed.xml', __FILE__))
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ require 'minitest_helper'
2
+ require 'ostruct'
3
+
4
+ class TestEpisodeFilter < ActiveSupport::TestCase
5
+ test 'should find episodes by number' do
6
+ config = OpenStruct.new(episodes: [1])
7
+ filter = Tapas::EpisodeFilter.new(collection, config)
8
+ assert_equal 1, filter.episodes.size
9
+ assert_equal 1, filter.episodes.first.number
10
+ end
11
+
12
+ test 'should find episode by title' do
13
+ config = OpenStruct.new(episodes: [], title: 'Barewords')
14
+ filter = Tapas::EpisodeFilter.new(collection, config)
15
+ assert_equal 1, filter.episodes.size
16
+ assert filter.episodes.first.title.include?('Barewords')
17
+ end
18
+
19
+ test 'should find episode by description' do
20
+ config = OpenStruct.new(episodes: [], description: 'readable')
21
+ filter = Tapas::EpisodeFilter.new(collection, config)
22
+ assert_equal 1, filter.episodes.size
23
+ assert filter.episodes.first.description.include?('readable')
24
+ end
25
+
26
+ test 'should find episodes by title or description' do
27
+ config = OpenStruct.new(episodes: [], search: 'cha')
28
+ filter = Tapas::EpisodeFilter.new(collection, config)
29
+ assert_equal 2, filter.episodes.size
30
+ assert filter.episodes.any? { |episode| episode.title.downcase.include?('cha') }
31
+ assert filter.episodes.any? { |episode| episode.description.downcase.include?('cha') }
32
+ end
33
+
34
+ test 'should find last episode' do
35
+ config = OpenStruct.new(last: true)
36
+ filter = Tapas::EpisodeFilter.new(collection, config)
37
+ assert_equal 1, filter.episodes.size
38
+ assert_equal 5, filter.episodes.first.number
39
+ end
40
+
41
+ test 'should find all episodes' do
42
+ config = OpenStruct.new(all: true)
43
+ filter = Tapas::EpisodeFilter.new(collection, config)
44
+ assert_equal collection.episodes.size, filter.episodes.size
45
+ end
46
+
47
+ def collection
48
+ stub(episodes:
49
+ [
50
+ Tapas::Episode.new(5, 'Array Literals', 'Composing command lines with fancy array literals.', 'https://rubytapas.dpdcart.com/feed/download/38/RubyTapas005.mp4'),
51
+ Tapas::Episode.new(4, 'Barewords', 'In this longer-than-usual episode, some thoughts on how to enable method logic to remain stable and unchanged while evolving and changing the source and scope of the values used by the logic.', 'https://rubytapas.dpdcart.com/feed/download/45/RubyTapas-Barewords.mp4'),
52
+ Tapas::Episode.new(3, 'Character Literals', 'Character literal syntax in Ruby.', 'https://rubytapas.dpdcart.com/feed/download/32/RubyTapas003.mp4'),
53
+ Tapas::Episode.new(2, 'Large Integer Literals', 'How to format big numbers so that they are readable.', 'https://rubytapas.dpdcart.com/feed/download/29/RubyTapas002.mp4'),
54
+ Tapas::Episode.new(1, 'Binary Literals', 'In this inaugural episode, a look at a handy syntax for writing out binary numbers.', 'https://rubytapas.dpdcart.com/feed/download/25/RubyTapas001.mp4'),
55
+ ]
56
+ )
57
+ end
58
+ end
@@ -0,0 +1,17 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestFeedSource < ActiveSupport::TestCase
4
+ test 'should read local file' do
5
+ config = Struct.new(:feed_file).new('foo_file')
6
+ source = Tapas::FeedSource.new(config)
7
+ File.expects(:read).with('foo_file').returns('bar')
8
+ assert_equal 'bar', source.feed
9
+ end
10
+
11
+ test 'should fetch authenticated feed url' do
12
+ config = Struct.new(:feed_file, :feed_url, :username, :password).new(nil, 'foo_url', 'user', 'pass')
13
+ source = Tapas::FeedSource.new(config)
14
+ source.expects(:open).with('foo_url', http_basic_authentication: ['user', 'pass']).returns(StringIO.new('baz'))
15
+ assert_equal 'baz', source.feed
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tapas
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Jon Riddle
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-progressbar
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: htmlentities
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '4.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '4.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 3.2.14
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 3.2.14
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 4.7.5
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 4.7.5
111
+ - !ruby/object:Gem::Dependency
112
+ name: mocha
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-debugger
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Download RubyTapas episodes
154
+ email:
155
+ - jon@jdrnetworking.com
156
+ executables:
157
+ - tapas
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - .gitignore
162
+ - .travis.yml
163
+ - Gemfile
164
+ - LICENSE.txt
165
+ - README.md
166
+ - Rakefile
167
+ - bin/tapas
168
+ - lib/tapas.rb
169
+ - lib/tapas/config.rb
170
+ - lib/tapas/downloader.rb
171
+ - lib/tapas/episode.rb
172
+ - lib/tapas/episode_collection.rb
173
+ - lib/tapas/episode_filter.rb
174
+ - lib/tapas/exceptions.rb
175
+ - lib/tapas/feed_source.rb
176
+ - lib/tapas/version.rb
177
+ - tapas.gemspec
178
+ - test/feed.xml
179
+ - test/minitest_helper.rb
180
+ - test/test_config.rb
181
+ - test/test_episode.rb
182
+ - test/test_episode_collection.rb
183
+ - test/test_episode_filter.rb
184
+ - test/test_feed_source.rb
185
+ homepage: https://github.com/jdrnetworking/tapas
186
+ licenses:
187
+ - MIT
188
+ metadata: {}
189
+ post_install_message:
190
+ rdoc_options: []
191
+ require_paths:
192
+ - lib
193
+ required_ruby_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 2.1.9
206
+ signing_key:
207
+ specification_version: 4
208
+ summary: tapas downloads the latest RubyTapas episode or episodes matching a given
209
+ search term
210
+ test_files:
211
+ - test/feed.xml
212
+ - test/minitest_helper.rb
213
+ - test/test_config.rb
214
+ - test/test_episode.rb
215
+ - test/test_episode_collection.rb
216
+ - test/test_episode_filter.rb
217
+ - test/test_feed_source.rb