podcast_finder 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/podcast_finder +5 -0
- data/bin/setup +8 -0
- data/lib/podcast_finder.rb +13 -0
- data/lib/podcast_finder/category.rb +52 -0
- data/lib/podcast_finder/commandline.rb +334 -0
- data/lib/podcast_finder/episode.rb +48 -0
- data/lib/podcast_finder/importer.rb +56 -0
- data/lib/podcast_finder/podcast.rb +63 -0
- data/lib/podcast_finder/scraper.rb +105 -0
- data/lib/podcast_finder/station.rb +34 -0
- data/lib/podcast_finder/version.rb +3 -0
- data/podcast_finder.gemspec +32 -0
- data/spec.md +17 -0
- metadata +157 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 99be2bb4a670ee66d6067cde58d75b45ed7edcd6
|
|
4
|
+
data.tar.gz: b3d6e25fcbaad3781761a703a7c866edcdec5aac
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b080f2533785256cfb497f5ab1e28299cfebad74298c18a3ea6d9af3e14e792ff4ea43063fd1ec749350f3ec4155824522ff139eec0ac553ecb5586e1c308796
|
|
7
|
+
data.tar.gz: a085a82fc54f3afa698a53682d5cf906aa8bcbb0c954ee3e81f82878590575fd7212f5138aa8660194ba2bf6beaabbc1936ba188c04a7657f426c589527a4cbf
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributor Code of Conduct
|
|
2
|
+
|
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
|
7
|
+
|
|
8
|
+
We are committed to making participation in this project a harassment-free
|
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
|
12
|
+
|
|
13
|
+
Examples of unacceptable behavior by participants include:
|
|
14
|
+
|
|
15
|
+
* The use of sexualized language or imagery
|
|
16
|
+
* Personal attacks
|
|
17
|
+
* Trolling or insulting/derogatory comments
|
|
18
|
+
* Public or private harassment
|
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
|
20
|
+
addresses, without explicit permission
|
|
21
|
+
* Other unethical or unprofessional conduct
|
|
22
|
+
|
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
27
|
+
threatening, offensive, or harmful.
|
|
28
|
+
|
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
|
32
|
+
Conduct may be permanently removed from the project team.
|
|
33
|
+
|
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
|
35
|
+
when an individual is representing the project or its community.
|
|
36
|
+
|
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
38
|
+
reported by contacting a project maintainer at elyse.klova@gmail.com. All
|
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
|
42
|
+
incident.
|
|
43
|
+
|
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
45
|
+
version 1.3.0, available at
|
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
|
47
|
+
|
|
48
|
+
[homepage]: http://contributor-covenant.org
|
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Elyse Klova
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# PodcastFinder
|
|
2
|
+
|
|
3
|
+
Podcast Finder is a CLI gem that displays ongoing podcasts by NPR and NPR-affiliated stations. Browse by category, view podcast descriptions and episodes, and get links to listen online.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'podcast_finder'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install podcast_finder
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Run using the command 'podcast_finder', then follow the command-line prompts to browse podcasts by category, view descriptions and recent episode lists, and get links to download and listen.
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
28
|
+
|
|
29
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
30
|
+
|
|
31
|
+
## Contributing
|
|
32
|
+
|
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/podcast_finder. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "podcast_finder"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start
|
data/bin/podcast_finder
ADDED
data/bin/setup
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative 'podcast_finder/version'
|
|
2
|
+
require 'date'
|
|
3
|
+
require 'open-uri'
|
|
4
|
+
require 'nokogiri'
|
|
5
|
+
require 'colorize'
|
|
6
|
+
|
|
7
|
+
require_relative 'podcast_finder/category'
|
|
8
|
+
require_relative 'podcast_finder/commandline'
|
|
9
|
+
require_relative 'podcast_finder/episode'
|
|
10
|
+
require_relative 'podcast_finder/importer'
|
|
11
|
+
require_relative 'podcast_finder/podcast'
|
|
12
|
+
require_relative 'podcast_finder/scraper'
|
|
13
|
+
require_relative 'podcast_finder/station'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class PodcastFinder::Category
|
|
2
|
+
|
|
3
|
+
attr_accessor :name, :url, :podcasts
|
|
4
|
+
|
|
5
|
+
@@all = []
|
|
6
|
+
|
|
7
|
+
def initialize(category_hash)
|
|
8
|
+
category_hash.each {|key, value| self.send("#{key}=", value)}
|
|
9
|
+
@podcasts = []
|
|
10
|
+
self.save
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def save
|
|
14
|
+
@@all << self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.all
|
|
18
|
+
@@all
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_podcast(podcast)
|
|
22
|
+
self.podcasts << podcast
|
|
23
|
+
podcast.add_category(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.create_from_collection(category_array)
|
|
27
|
+
category_array.each {|category_hash| self.new(category_hash)}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.list_categories
|
|
31
|
+
self.all.each_with_index do |category, index|
|
|
32
|
+
puts "(#{index + 1}) #{category.name}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def list_podcasts(number)
|
|
37
|
+
counter = 1 + number
|
|
38
|
+
podcast_list_count = 0
|
|
39
|
+
until counter > (number + 10) do
|
|
40
|
+
if counter <= self.podcasts.size
|
|
41
|
+
podcast = self.podcasts[counter - 1]
|
|
42
|
+
puts "(#{counter}) #{podcast.name}"
|
|
43
|
+
counter += 1
|
|
44
|
+
podcast_list_count += 1
|
|
45
|
+
else
|
|
46
|
+
counter += 10
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
podcast_list_count
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
class PodcastFinder::CLI
|
|
2
|
+
|
|
3
|
+
attr_accessor :quit, :podcast_counter, :category_choice, :input, :podcast_choice, :episode_choice
|
|
4
|
+
|
|
5
|
+
def initialize(quit = "NO")
|
|
6
|
+
@quit = quit
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
self.startup_sequence
|
|
11
|
+
until @quit == "YES"
|
|
12
|
+
self.browse_all_categories
|
|
13
|
+
end
|
|
14
|
+
puts "Thanks for using the Command Line Podcast Finder!"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#methods needed for startup
|
|
18
|
+
|
|
19
|
+
def startup_sequence
|
|
20
|
+
puts "Setting up your command line podcast finder...".colorize(:light_red)
|
|
21
|
+
self.start_import
|
|
22
|
+
sleep(0.5)
|
|
23
|
+
puts ".".colorize(:light_red)
|
|
24
|
+
sleep(0.5)
|
|
25
|
+
puts ".".colorize(:light_yellow)
|
|
26
|
+
sleep(0.5)
|
|
27
|
+
puts ".".colorize(:light_green)
|
|
28
|
+
sleep(0.5)
|
|
29
|
+
puts "Setup complete.".colorize(:light_green)
|
|
30
|
+
sleep(0.5)
|
|
31
|
+
puts ""
|
|
32
|
+
puts "Welcome to the Command Line Podcast Finder!"
|
|
33
|
+
puts "You can use this command line gem to find and listen to interesting podcasts produced by NPR and affiliated stations."
|
|
34
|
+
sleep(0.5)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def start_import
|
|
38
|
+
PodcastFinder::DataImporter.import_categories('http://www.npr.org/podcasts')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#basic menu display methods
|
|
42
|
+
|
|
43
|
+
def help
|
|
44
|
+
puts ""
|
|
45
|
+
puts "Help: Commands".colorize(:light_blue)
|
|
46
|
+
puts "--To access any numbered menu, simply type the number of the item you're selecting and press Enter to confirm."
|
|
47
|
+
puts " Example Menu: All Categories"
|
|
48
|
+
puts " (1) Arts"
|
|
49
|
+
puts " (2) Business"
|
|
50
|
+
puts " (3) Comedy"
|
|
51
|
+
puts " For example: if you want to view the Comedy category, just type '3' (without the quotes) and press Enter"
|
|
52
|
+
puts "--Type 'exit' at any time to quit the browser"
|
|
53
|
+
puts "--Type 'menu' at any time to go back to the main category menu"
|
|
54
|
+
puts "--Type 'help' if you need a quick reminder about the commands"
|
|
55
|
+
puts "To proceed, enter a command from above or type 'back' to return to what you were doing".colorize(:light_blue)
|
|
56
|
+
self.choose_after_help
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def choose_after_help
|
|
60
|
+
self.get_input
|
|
61
|
+
if @input == "MENU" || @input == "EXIT" || @input == "HELP" || @input == "BACK"
|
|
62
|
+
self.proceed_based_on_input
|
|
63
|
+
else
|
|
64
|
+
puts "Sorry, that's not an option. Please type a command from the help menu or type 'back' to return to what you were doing".colorize(:light_blue)
|
|
65
|
+
self.choose_after_help
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#methods for gets-ing, parsing and acting based on user input
|
|
70
|
+
|
|
71
|
+
def get_input
|
|
72
|
+
input = gets.strip
|
|
73
|
+
self.parse_input(input)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_input(input)
|
|
77
|
+
if input.match(/^\d+$/)
|
|
78
|
+
@input = input.to_i
|
|
79
|
+
elsif input.upcase == "HELP" || input.upcase == "MENU" || input.upcase == "EXIT" || input.upcase == "MORE" || input.upcase == "BACK" || input.upcase == "PODCASTS"
|
|
80
|
+
@input = input.upcase
|
|
81
|
+
else
|
|
82
|
+
@input = "STUCK"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def proceed_based_on_input
|
|
87
|
+
case @input
|
|
88
|
+
when "STUCK"
|
|
89
|
+
puts "Sorry, that's not an option. Please type a command from the options above. Stuck? Type 'help'.".colorize(:light_blue)
|
|
90
|
+
when "HELP"
|
|
91
|
+
self.help
|
|
92
|
+
when "MENU"
|
|
93
|
+
self.browse_all_categories
|
|
94
|
+
when "EXIT"
|
|
95
|
+
@quit = "YES"
|
|
96
|
+
when @input == "BACK" || @input == "MORE" || @input == "PODCASTS"
|
|
97
|
+
@input
|
|
98
|
+
when @input == Fixnum && @input >= 1
|
|
99
|
+
@input
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#methods for browsing categories and viewing podcasts
|
|
104
|
+
|
|
105
|
+
def browse_all_categories
|
|
106
|
+
@podcast_counter = 0
|
|
107
|
+
puts ""
|
|
108
|
+
puts "Main Menu: All Categories".colorize(:light_blue)
|
|
109
|
+
PodcastFinder::Category.list_categories
|
|
110
|
+
puts ""
|
|
111
|
+
puts "To get started, choose a category above (1-#{PodcastFinder::Category.all.size}) or type 'help' to see a list of commands.".colorize(:light_blue)
|
|
112
|
+
puts "You can also type 'exit' at any point to quit.".colorize(:light_blue)
|
|
113
|
+
self.choose_category
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def choose_category
|
|
117
|
+
self.get_input
|
|
118
|
+
if @input.class == Fixnum && @input.between?(1, 16)
|
|
119
|
+
@category_choice = PodcastFinder::Category.all[@input - 1]
|
|
120
|
+
puts "Loading podcasts from #{@category_choice.name}, please wait..."
|
|
121
|
+
PodcastFinder::DataImporter.import_podcast_data(@category_choice)
|
|
122
|
+
self.browse_category
|
|
123
|
+
else
|
|
124
|
+
if @input.class == Fixnum && !@input.between?(1,16)
|
|
125
|
+
puts "Sorry, that's not a category. Please enter a number between 1 and 16"
|
|
126
|
+
self.choose_category
|
|
127
|
+
else
|
|
128
|
+
@input = "STUCK" unless @input == "EXIT" || @input == "HELP" || @input == "BACK"
|
|
129
|
+
if @input == "BACK"
|
|
130
|
+
@input = "MENU"
|
|
131
|
+
elsif @input == "HELP"
|
|
132
|
+
self.proceed_based_on_input
|
|
133
|
+
if @input == "BACK"
|
|
134
|
+
self.browse_all_categories
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
self.proceed_based_on_input
|
|
138
|
+
self.choose_category unless @quit == "YES"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def browse_category
|
|
144
|
+
puts ""
|
|
145
|
+
puts "Category: #{@category_choice.name}".colorize(:light_blue)
|
|
146
|
+
self.display_podcasts
|
|
147
|
+
self.choose_podcast
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def display_podcasts
|
|
151
|
+
@listed_podcasts = @category_choice.list_podcasts(@podcast_counter)
|
|
152
|
+
if @listed_podcasts == 10 && @category_choice.podcasts.size > @podcast_counter + @listed_podcasts
|
|
153
|
+
puts ""
|
|
154
|
+
puts "Enter the number of the podcast you'd like to check out (1-#{@podcast_counter + @listed_podcasts})".colorize(:light_blue)
|
|
155
|
+
puts "Type 'menu' to return to the category list".colorize(:light_blue)
|
|
156
|
+
puts "Type 'more' to see the next 10 podcasts".colorize(:light_blue)
|
|
157
|
+
else
|
|
158
|
+
puts ""
|
|
159
|
+
puts "That's all the podcasts for this category!".colorize(:light_blue)
|
|
160
|
+
puts "Enter the number of the podcast you'd like to check out (1-#{@podcast_counter + @listed_podcasts})".colorize(:light_blue)
|
|
161
|
+
puts "Type 'menu' to return to the category list".colorize(:light_blue)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def choose_podcast
|
|
166
|
+
self.get_input
|
|
167
|
+
if @input.class == Fixnum && @input.between?(1, @podcast_counter + @listed_podcasts)
|
|
168
|
+
@podcast_choice = @category_choice.podcasts[@input - 1]
|
|
169
|
+
self.display_podcast_info
|
|
170
|
+
elsif @input.class == Fixnum && !@input.between?(1, @podcast_counter + @listed_podcasts)
|
|
171
|
+
puts "Sorry, that's not an option. Please choose a number that corresponds to a podcast.".colorize(:light_blue)
|
|
172
|
+
self.choose_podcast
|
|
173
|
+
elsif @input == "MENU"
|
|
174
|
+
@category_choice = nil
|
|
175
|
+
self.proceed_based_on_input
|
|
176
|
+
elsif @input == "MORE"
|
|
177
|
+
@podcast_counter += 10
|
|
178
|
+
self.browse_category
|
|
179
|
+
else
|
|
180
|
+
@input = "STUCK" unless @input == "EXIT" || @input == "HELP" || @input == "BACK"
|
|
181
|
+
if @input == "BACK"
|
|
182
|
+
@input = "MENU"
|
|
183
|
+
elsif @input == "HELP"
|
|
184
|
+
self.proceed_based_on_input
|
|
185
|
+
if @input == "BACK"
|
|
186
|
+
self.browse_category
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
self.proceed_based_on_input
|
|
190
|
+
self.choose_podcast unless @quit == "YES"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#methods for getting details on a specific podcast
|
|
195
|
+
def display_podcast_info
|
|
196
|
+
puts "Loading details for #{@podcast_choice.name}..."
|
|
197
|
+
PodcastFinder::DataImporter.import_description(@podcast_choice)
|
|
198
|
+
puts ""
|
|
199
|
+
@podcast_choice.list_data
|
|
200
|
+
puts ""
|
|
201
|
+
puts "Choose an option below to proceed:".colorize(:light_blue)
|
|
202
|
+
puts "Type 'more' to get episode list".colorize(:light_blue)
|
|
203
|
+
puts "Type 'back' to return to podcast listing for #{@category_choice.name}".colorize(:light_blue)
|
|
204
|
+
puts "Type 'menu' to return to main category menu".colorize(:light_blue)
|
|
205
|
+
self.choose_podcast_action
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def choose_podcast_action
|
|
209
|
+
self.get_input
|
|
210
|
+
if @input == "MORE"
|
|
211
|
+
self.display_episode_list
|
|
212
|
+
elsif @input == "BACK"
|
|
213
|
+
@podcast_counter = 0
|
|
214
|
+
self.browse_category
|
|
215
|
+
elsif @input == "MENU"
|
|
216
|
+
self.proceed_based_on_input
|
|
217
|
+
else
|
|
218
|
+
@input = "STUCK" unless @input == "EXIT" || @input == "HELP"
|
|
219
|
+
if @input == "HELP"
|
|
220
|
+
self.proceed_based_on_input
|
|
221
|
+
if @input == "BACK"
|
|
222
|
+
self.display_podcast_info
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
self.proceed_based_on_input
|
|
226
|
+
self.choose_podcast_action unless @quit == "YES"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def display_episode_list
|
|
231
|
+
puts "Getting episodes for #{@podcast_choice.name}..."
|
|
232
|
+
PodcastFinder::DataImporter.import_episodes(@podcast_choice)
|
|
233
|
+
if !@podcast_choice.episodes.empty?
|
|
234
|
+
puts ""
|
|
235
|
+
puts "#{@podcast_choice.name} Recent Episode List".colorize(:light_blue)
|
|
236
|
+
@podcast_choice.list_episodes
|
|
237
|
+
puts ""
|
|
238
|
+
puts "These are all the options currently available in Podcast Finder.".colorize(:light_blue)
|
|
239
|
+
puts "To see more, check out #{@podcast_choice.name} online at #{@podcast_choice.url}"
|
|
240
|
+
puts ""
|
|
241
|
+
puts "Options:".colorize(:light_blue)
|
|
242
|
+
puts "Select an episode (1-#{@podcast_choice.episodes.count}) to get a description and download link".colorize(:light_blue)
|
|
243
|
+
puts "Type 'back' to return to podcast listing for #{@category_choice.name}".colorize(:light_blue)
|
|
244
|
+
puts "Type 'menu' to see the category list".colorize(:light_blue)
|
|
245
|
+
self.choose_episode
|
|
246
|
+
else #for edge case where a podcast has no associated episodes but is listed as active by website
|
|
247
|
+
puts ""
|
|
248
|
+
puts "Looks like #{@podcast_choice.name} doesn't have episodes online.".colorize(:light_red)
|
|
249
|
+
puts ""
|
|
250
|
+
puts "Type 'back' to return to podcast listing for #{@category_choice.name}".colorize(:light_blue)
|
|
251
|
+
puts "Type 'menu' to see the category list".colorize(:light_blue)
|
|
252
|
+
self.choose_action_no_episodes
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def choose_action_no_episodes
|
|
257
|
+
self.get_input
|
|
258
|
+
if @input == "BACK"
|
|
259
|
+
@podcast_counter = 0
|
|
260
|
+
self.browse_category
|
|
261
|
+
elsif @input == "MENU"
|
|
262
|
+
self.proceed_based_on_input
|
|
263
|
+
else
|
|
264
|
+
@input = "STUCK" unless @input == "EXIT" || @input == "HELP"
|
|
265
|
+
if @input == "HELP"
|
|
266
|
+
self.proceed_based_on_input
|
|
267
|
+
if @input == "BACK"
|
|
268
|
+
self.display_episode_list
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
self.proceed_based_on_input
|
|
272
|
+
self.choose_action_no_episodes unless @quit == "YES"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def choose_episode
|
|
277
|
+
self.get_input
|
|
278
|
+
if @input.class == Fixnum && @input.between?(1, @podcast_choice.episodes.count)
|
|
279
|
+
@episode_choice = @podcast_choice.episodes[@input-1]
|
|
280
|
+
self.display_episode_info
|
|
281
|
+
elsif @input.class == Fixnum && !@input.between?(1, @podcast_choice.episodes.count)
|
|
282
|
+
puts "Sorry, that's not an episode option. Please enter a number between 1 and #{@podcast_choice.episodes.count} to proceed."
|
|
283
|
+
self.choose_episode
|
|
284
|
+
elsif @input == "BACK"
|
|
285
|
+
@podcast_counter = 0
|
|
286
|
+
self.browse_category
|
|
287
|
+
else
|
|
288
|
+
@input = "STUCK" unless @input == "EXIT" || @input == "HELP"
|
|
289
|
+
if @input == "HELP"
|
|
290
|
+
self.proceed_based_on_input
|
|
291
|
+
if @input == "BACK"
|
|
292
|
+
self.display_episode_list
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
self.proceed_based_on_input
|
|
296
|
+
self.choose_episode unless @quit == "YES"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def display_episode_info
|
|
301
|
+
puts ""
|
|
302
|
+
@episode_choice.list_data
|
|
303
|
+
puts ""
|
|
304
|
+
puts "Options:".colorize(:light_blue)
|
|
305
|
+
puts "Type 'back' to return to episode listing for #{@podcast_choice.name}".colorize(:light_blue)
|
|
306
|
+
puts "Type 'podcasts' to return to the podcast list for #{@category_choice.name}".colorize(:light_blue)
|
|
307
|
+
puts "Type 'menu' to see the category list".colorize(:light_blue)
|
|
308
|
+
self.choose_action_episode_info
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def choose_action_episode_info
|
|
312
|
+
self.get_input
|
|
313
|
+
if @input == "BACK"
|
|
314
|
+
@episode_choice = nil
|
|
315
|
+
self.display_episode_list
|
|
316
|
+
elsif @input == "PODCASTS"
|
|
317
|
+
@podcast_counter = 0
|
|
318
|
+
@podcast_choice = nil
|
|
319
|
+
@episode_choice = nil
|
|
320
|
+
self.browse_category
|
|
321
|
+
else
|
|
322
|
+
@input = "STUCK" unless @input == "EXIT" || @input == "HELP"
|
|
323
|
+
if @input == "HELP"
|
|
324
|
+
self.proceed_based_on_input
|
|
325
|
+
if @input == "BACK"
|
|
326
|
+
self.display_episode_info
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
self.proceed_based_on_input
|
|
330
|
+
self.choose_action_episode_info unless @quit == "YES"
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class PodcastFinder::Episode
|
|
2
|
+
|
|
3
|
+
attr_accessor :date, :title, :description, :download_link, :length, :podcast
|
|
4
|
+
|
|
5
|
+
@@all = []
|
|
6
|
+
|
|
7
|
+
def initialize(episode_hash)
|
|
8
|
+
episode_hash.each {|key, value| self.send("#{key}=", value)}
|
|
9
|
+
self.format_date
|
|
10
|
+
self.save
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format_date
|
|
14
|
+
date_string = @date.to_s
|
|
15
|
+
@date = Date.parse(date_string, "%Y-%m-%d")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def display_date
|
|
19
|
+
@date.strftime('%B %-d, %Y')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.create_from_collection(episode_array)
|
|
23
|
+
episode_array.each {|episode_hash| self.new(episode_hash)}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_data
|
|
27
|
+
puts "Episode: #{self.title}".colorize(:light_blue)
|
|
28
|
+
puts "Podcast: ".colorize(:light_blue) + "#{self.podcast.name}"
|
|
29
|
+
puts "Date ".colorize(:light_blue) + "#{self.display_date}"
|
|
30
|
+
if self.length.nil?
|
|
31
|
+
puts "Length: ".colorize(:light_blue) + "Not Available"
|
|
32
|
+
else
|
|
33
|
+
puts "Length: ".colorize(:light_blue) + "#{self.length}"
|
|
34
|
+
end
|
|
35
|
+
puts "Description: ".colorize(:light_blue) + "#{self.description}"
|
|
36
|
+
puts "Link to download: ".colorize(:light_blue) + "#{self.download_link}"
|
|
37
|
+
puts "Link to listen: ".colorize(:light_blue) + "#{self.podcast.url}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def save
|
|
41
|
+
@@all << self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.all
|
|
45
|
+
@@all
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class PodcastFinder::DataImporter
|
|
2
|
+
|
|
3
|
+
def self.import_categories(index_url)
|
|
4
|
+
category_data = PodcastFinder::Scraper.new.scrape_category_list(index_url)
|
|
5
|
+
PodcastFinder::Category.create_from_collection(category_data)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.import_podcast_data(category)
|
|
9
|
+
podcast_array = PodcastFinder::Scraper.new.scrape_podcasts(category.url)
|
|
10
|
+
self.import_stations(podcast_array)
|
|
11
|
+
self.import_podcasts(podcast_array, category)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#helper methods for import_podcast_data
|
|
15
|
+
|
|
16
|
+
def self.import_stations(podcast_array)
|
|
17
|
+
podcast_array.each do |podcast_hash|
|
|
18
|
+
check_station = podcast_hash[:station]
|
|
19
|
+
if PodcastFinder::Station.find_by_name(check_station).nil?
|
|
20
|
+
station = PodcastFinder::Station.new(podcast_hash)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.import_podcasts(podcast_array, category)
|
|
26
|
+
podcast_array.each do |podcast_hash|
|
|
27
|
+
check_podcast = podcast_hash[:name]
|
|
28
|
+
if PodcastFinder::Podcast.find_by_name(check_podcast).nil?
|
|
29
|
+
podcast = PodcastFinder::Podcast.new(podcast_hash)
|
|
30
|
+
category.add_podcast(podcast)
|
|
31
|
+
station = PodcastFinder::Station.find_by_name(podcast_hash[:station])
|
|
32
|
+
station.add_podcast(podcast)
|
|
33
|
+
else
|
|
34
|
+
podcast = PodcastFinder::Podcast.find_by_name(check_podcast)
|
|
35
|
+
category.add_podcast(podcast)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.import_description(podcast)
|
|
41
|
+
if podcast.description.nil?
|
|
42
|
+
podcast.description = PodcastFinder::Scraper.new.get_podcast_description(podcast.url)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.import_episodes(podcast)
|
|
47
|
+
if podcast.episodes == []
|
|
48
|
+
episode_list = PodcastFinder::Scraper.new.scrape_episodes(podcast.url)
|
|
49
|
+
episode_list.each do |episode_hash|
|
|
50
|
+
episode = PodcastFinder::Episode.new(episode_hash)
|
|
51
|
+
podcast.add_episode(episode)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
class PodcastFinder::Podcast
|
|
2
|
+
|
|
3
|
+
attr_accessor :name, :url
|
|
4
|
+
attr_reader :station, :categories, :description, :episodes
|
|
5
|
+
|
|
6
|
+
@@all = []
|
|
7
|
+
|
|
8
|
+
def initialize(podcast_hash)
|
|
9
|
+
@name = podcast_hash[:name]
|
|
10
|
+
@url = podcast_hash[:url]
|
|
11
|
+
@description = nil
|
|
12
|
+
@categories = []
|
|
13
|
+
@episodes = []
|
|
14
|
+
self.save
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_category(category)
|
|
18
|
+
if category.class == PodcastFinder::Category && !self.categories.include?(category)
|
|
19
|
+
@categories << category
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_episode(episode)
|
|
24
|
+
self.episodes << episode
|
|
25
|
+
episode.podcast = self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def station=(station)
|
|
29
|
+
if station.class == PodcastFinder::Station
|
|
30
|
+
@station = station
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list_episodes
|
|
35
|
+
self.episodes.each_with_index do |episode, index|
|
|
36
|
+
puts "(#{index + 1}) #{episode.title} - #{episode.display_date}" + "#{" - " + episode.length unless episode.length.nil?}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def description=(description)
|
|
41
|
+
@description = description
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def list_data
|
|
45
|
+
puts "Podcast: #{self.name}".colorize(:light_blue)
|
|
46
|
+
puts "Station:".colorize(:light_blue) + "#{self.station.name}"
|
|
47
|
+
puts "Description:".colorize(:light_blue) + " #{self.description}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def save
|
|
52
|
+
@@all << self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.all
|
|
56
|
+
@@all
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.find_by_name(name)
|
|
60
|
+
self.all.detect {|item| item.name == name}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
class PodcastFinder::Scraper
|
|
2
|
+
|
|
3
|
+
attr_accessor :index, :categories
|
|
4
|
+
|
|
5
|
+
def scrape_page(url)
|
|
6
|
+
html = open(url)
|
|
7
|
+
@index = Nokogiri::HTML(html)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def scrape_category_list(url)
|
|
11
|
+
self.scrape_page(url)
|
|
12
|
+
@categories = []
|
|
13
|
+
groups = @index.css('nav.global-navigation div.subnav.subnav-podcast-categories div.group')
|
|
14
|
+
categories = groups.each do |group|
|
|
15
|
+
category_info = group.css('ul li')
|
|
16
|
+
category_info.each do |category_data|
|
|
17
|
+
category = {
|
|
18
|
+
:name => category_data.css('a').text,
|
|
19
|
+
:url => "http://www.npr.org" + category_data.css('a').attribute('href').value
|
|
20
|
+
}
|
|
21
|
+
@categories << category
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
@categories
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# podcasts scraping
|
|
28
|
+
|
|
29
|
+
def scrape_podcasts(category_url)
|
|
30
|
+
counter = 1
|
|
31
|
+
podcasts = []
|
|
32
|
+
until counter == "done" do
|
|
33
|
+
scrape_url = category_url + "/partials?start=#{counter}"
|
|
34
|
+
self.scrape_page(scrape_url)
|
|
35
|
+
if !@index.css('article').first.nil?
|
|
36
|
+
active_podcasts = @index.css('article.podcast-active')
|
|
37
|
+
active_podcasts.each {|podcast| podcasts << self.get_podcast_data(podcast)}
|
|
38
|
+
counter += @index.css('article').size
|
|
39
|
+
else
|
|
40
|
+
counter = "done"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
podcasts
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def get_podcast_data(podcast)
|
|
47
|
+
data = {
|
|
48
|
+
:name => podcast.css('h1.title a').text,
|
|
49
|
+
:url => podcast.css('h1.title a').attribute('href').value,
|
|
50
|
+
:station => podcast.css('h3.org a').text,
|
|
51
|
+
:station_url => "http://www.npr.org" + podcast.css('h3.org a').attribute('href').value
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get_podcast_description(podcast_url)
|
|
56
|
+
self.scrape_page(podcast_url)
|
|
57
|
+
if @index.css('div.detail-overview-content.col2 p').size == 1
|
|
58
|
+
text = @index.css('div.detail-overview-content.col2 p').text
|
|
59
|
+
elsif @indext.css('div.detail-overview-content.col2 p') > 1
|
|
60
|
+
text = @index.css('div.detail-overview-content.col2 p').first.text
|
|
61
|
+
end
|
|
62
|
+
description = text.gsub(@index.css('div.detail-overview-content.col2 p a.more').text, "").gsub("\"", "'")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#individual episode methods
|
|
66
|
+
|
|
67
|
+
def scrape_episodes(podcast_url)
|
|
68
|
+
episode_list = []
|
|
69
|
+
self.scrape_page(podcast_url)
|
|
70
|
+
episodes = @index.css('section.podcast-section.episode-list article.item.podcast-episode')
|
|
71
|
+
episodes.each do |episode|
|
|
72
|
+
episode_data = self.get_episode_data(episode)
|
|
73
|
+
episode_list << episode_data unless episode_data[:download_link].nil? #unless is for edge case
|
|
74
|
+
end
|
|
75
|
+
episode_list
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_episode_data(episode)
|
|
79
|
+
#for an edge case where sometimes the first podcast has no file associated with it
|
|
80
|
+
if !episode.css('div.audio-module-tools').empty?
|
|
81
|
+
link = episode.css('div.audio-module-tools ul li a').attribute('href').value
|
|
82
|
+
length = episode.css('div.audio-module-controls b.audio-module-listen-duration').text[/(\d*:?\d{1,2}:\d\d)/]
|
|
83
|
+
else
|
|
84
|
+
link = nil
|
|
85
|
+
length = nil
|
|
86
|
+
end
|
|
87
|
+
if episode.css('p').count > 1
|
|
88
|
+
paragraphs = episode.css('p')
|
|
89
|
+
p1 = paragraphs[0].text.gsub(episode.css('p.teaser time').text, "").gsub(/\n+\s*/, "").gsub("\"", "'")
|
|
90
|
+
p2 = paragraphs[1].text.gsub(/\n+\s*/, "").gsub("\"", "'")
|
|
91
|
+
description = p1 + p2 + " Read more online >>"
|
|
92
|
+
else
|
|
93
|
+
description = episode.css('p.teaser').text.gsub(episode.css('p.teaser time').text, "").gsub(/\n+\s*/, "").gsub("\"", "'")
|
|
94
|
+
end
|
|
95
|
+
#end edge case
|
|
96
|
+
episode_data = {
|
|
97
|
+
:date => episode.css('time').attribute('datetime').value,
|
|
98
|
+
:title => episode.css('h2.title').text.gsub(/\n+\s*/, "").gsub("\"", "'"),
|
|
99
|
+
:length => length,
|
|
100
|
+
:description => description,
|
|
101
|
+
:download_link => link
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class PodcastFinder::Station
|
|
2
|
+
|
|
3
|
+
attr_accessor :name, :url
|
|
4
|
+
attr_reader :podcasts
|
|
5
|
+
|
|
6
|
+
@@all = []
|
|
7
|
+
|
|
8
|
+
def initialize(podcast_hash)
|
|
9
|
+
@name = podcast_hash[:station]
|
|
10
|
+
@url = podcast_hash[:station_url]
|
|
11
|
+
@podcasts = []
|
|
12
|
+
self.save
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_podcast(podcast)
|
|
16
|
+
if podcast.class == PodcastFinder::Podcast
|
|
17
|
+
@podcasts << podcast
|
|
18
|
+
podcast.station = self
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def save
|
|
23
|
+
@@all << self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.all
|
|
27
|
+
@@all
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.find_by_name(name)
|
|
31
|
+
self.all.detect {|item| item.name == name}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
@@ -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 'podcast_finder/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'podcast_finder'
|
|
8
|
+
spec.version = PodcastFinder::VERSION
|
|
9
|
+
spec.license = 'MIT'
|
|
10
|
+
|
|
11
|
+
spec.summary = "CLI gem for finding NPR podcasts"
|
|
12
|
+
spec.description =
|
|
13
|
+
"Podcast Finder is a CLI gem that displays ongoing podcasts by NPR and NPR-affiliated stations. Browse by category, view podcast descriptions and episodes, and get links to listen online."
|
|
14
|
+
spec.authors = ["Elyse Klova"]
|
|
15
|
+
spec.email = 'elyse.klova@gmail.com'
|
|
16
|
+
|
|
17
|
+
spec.homepage = 'https://github.com/klovae/podcast-finder-gem'
|
|
18
|
+
|
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
20
|
+
spec.executables = ['podcast_finder']
|
|
21
|
+
spec.require_paths = ['lib']
|
|
22
|
+
|
|
23
|
+
spec.post_install_message = "Thanks for installing! Happy listening."
|
|
24
|
+
|
|
25
|
+
spec.add_runtime_dependency 'nokogiri', '>= 0'
|
|
26
|
+
spec.add_runtime_dependency 'colorize', '~> 0.8', '>= 0.8.1'
|
|
27
|
+
|
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.5'
|
|
29
|
+
spec.add_development_dependency 'pry', '>= 0'
|
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
32
|
+
end
|
data/spec.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Specifications for the CLI Assessment
|
|
2
|
+
|
|
3
|
+
Specs:
|
|
4
|
+
- [x] Have a CLI for interfacing with the application
|
|
5
|
+
Created a user-friendly CLI with a main menu and help menu, that allows the user to go back and forth between levels of data and exit at any time from any point in the program.
|
|
6
|
+
- [x] Pull data from an external source
|
|
7
|
+
Uses Nokogiri to scrape content from NPR's Podcast Directory
|
|
8
|
+
- Pulls category data from http://www.npr.org/podcasts
|
|
9
|
+
- Pulls podcast data from individual category pages
|
|
10
|
+
- Pulls episode data from individual podcast pages
|
|
11
|
+
- [x] Implement both list and detail views
|
|
12
|
+
- Uses OO to associate podcasts with categories and episodes with podcasts, so that the user can view any of the following:
|
|
13
|
+
- list of categories
|
|
14
|
+
- lists of podcasts by category
|
|
15
|
+
- descriptions of podcasts
|
|
16
|
+
- lists of episodes for podcasts
|
|
17
|
+
- descriptions of individual episodes, their runtimes, and links to listen and download when available
|
metadata
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: podcast_finder
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.14
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Elyse Klova
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2016-09-26 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: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: colorize
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.8'
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: 0.8.1
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - "~>"
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0.8'
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 0.8.1
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rspec
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.5'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.5'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: pry
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: bundler
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.12'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.12'
|
|
89
|
+
- !ruby/object:Gem::Dependency
|
|
90
|
+
name: rake
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '10.0'
|
|
96
|
+
type: :development
|
|
97
|
+
prerelease: false
|
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '10.0'
|
|
103
|
+
description: Podcast Finder is a CLI gem that displays ongoing podcasts by NPR and
|
|
104
|
+
NPR-affiliated stations. Browse by category, view podcast descriptions and episodes,
|
|
105
|
+
and get links to listen online.
|
|
106
|
+
email: elyse.klova@gmail.com
|
|
107
|
+
executables:
|
|
108
|
+
- podcast_finder
|
|
109
|
+
extensions: []
|
|
110
|
+
extra_rdoc_files: []
|
|
111
|
+
files:
|
|
112
|
+
- ".gitignore"
|
|
113
|
+
- ".travis.yml"
|
|
114
|
+
- CODE_OF_CONDUCT.md
|
|
115
|
+
- Gemfile
|
|
116
|
+
- LICENSE.txt
|
|
117
|
+
- README.md
|
|
118
|
+
- Rakefile
|
|
119
|
+
- bin/console
|
|
120
|
+
- bin/podcast_finder
|
|
121
|
+
- bin/setup
|
|
122
|
+
- lib/podcast_finder.rb
|
|
123
|
+
- lib/podcast_finder/category.rb
|
|
124
|
+
- lib/podcast_finder/commandline.rb
|
|
125
|
+
- lib/podcast_finder/episode.rb
|
|
126
|
+
- lib/podcast_finder/importer.rb
|
|
127
|
+
- lib/podcast_finder/podcast.rb
|
|
128
|
+
- lib/podcast_finder/scraper.rb
|
|
129
|
+
- lib/podcast_finder/station.rb
|
|
130
|
+
- lib/podcast_finder/version.rb
|
|
131
|
+
- podcast_finder.gemspec
|
|
132
|
+
- spec.md
|
|
133
|
+
homepage: https://github.com/klovae/podcast-finder-gem
|
|
134
|
+
licenses:
|
|
135
|
+
- MIT
|
|
136
|
+
metadata: {}
|
|
137
|
+
post_install_message: Thanks for installing! Happy listening.
|
|
138
|
+
rdoc_options: []
|
|
139
|
+
require_paths:
|
|
140
|
+
- lib
|
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
requirements: []
|
|
152
|
+
rubyforge_project:
|
|
153
|
+
rubygems_version: 2.6.4
|
|
154
|
+
signing_key:
|
|
155
|
+
specification_version: 4
|
|
156
|
+
summary: CLI gem for finding NPR podcasts
|
|
157
|
+
test_files: []
|