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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in podcast_finder.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/podcast_finder'
4
+
5
+ PodcastFinder::CLI.new.call
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,3 @@
1
+ class PodcastFinder
2
+ VERSION = "0.1.14"
3
+ 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: []