retro_casts 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +25 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -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 +122 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/retrocasts +6 -0
- data/lib/retro_casts.rb +94 -0
- data/lib/retro_casts/cli.rb +33 -0
- data/lib/retro_casts/episode.rb +14 -0
- data/lib/retro_casts/null_website.rb +16 -0
- data/lib/retro_casts/rails_casts.rb +138 -0
- data/lib/retro_casts/version.rb +3 -0
- data/lib/retro_casts/website.rb +43 -0
- data/retro_casts.gemspec +32 -0
- data/spec.md +6 -0
- metadata +194 -0
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 snsavage@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 Scott Savage
|
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,122 @@
|
|
1
|
+
# RetroCasts
|
2
|
+
|
3
|
+
[![Code Climate](https://codeclimate.com/github/snsavage/retro_casts/badges/gpa.svg)](https://codeclimate.com/github/snsavage/retro_casts) [![Test Coverage](https://codeclimate.com/github/snsavage/retro_casts/badges/coverage.svg)](https://codeclimate.com/github/snsavage/retro_casts/coverage) [![Issue Count](https://codeclimate.com/github/snsavage/retro_casts/badges/issue_count.svg)](https://codeclimate.com/github/snsavage/retro_casts)
|
4
|
+
|
5
|
+
```
|
6
|
+
_____ _ _____ _
|
7
|
+
| __ \ | | / ____| | |
|
8
|
+
| |__) |___| |_ _ __ ___ | | __ _ ___| |_ ___
|
9
|
+
| _ // _ \ __| '__/ _ \| | / _` / __| __/ __|
|
10
|
+
| | \ \ __/ |_| | | (_) | |___| (_| \__ \ |_\__ \
|
11
|
+
|_| \_\___|\__|_| \___/ \_____\__,_|___/\__|___/
|
12
|
+
```
|
13
|
+
The RetroCasts gem provides access to [RailsCasts.com](http://railscasts.com) through a command line utility. The utility allows browsing and searching of RailsCasts episodes with the option to view a description of the episode and to open the episode in your web browser. See usage below for more detail.
|
14
|
+
|
15
|
+
*RetroCasts was created as a project for the Flatiron School's Online Web Developer program. You can find more information here: [learn.co/with/snavage](http://learn.co/with/snavage). Or to find out more about the making of RetroCasts checkout my post at [snsavage.com](http://www.snsavage.com/blog/2016/the_making_of_retrocasts.html)*
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
To install run:
|
20
|
+
|
21
|
+
$ gem install retro_casts
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
To access RetroCasts run the ```retrocasts``` command.
|
26
|
+
|
27
|
+
$ retrocasts
|
28
|
+
|
29
|
+
This will open the episode menu.
|
30
|
+
|
31
|
+
```
|
32
|
+
_____ _ _____ _
|
33
|
+
| __ \ | | / ____| | |
|
34
|
+
| |__) |___| |_ _ __ ___ | | __ _ ___| |_ ___
|
35
|
+
| _ // _ \ __| '__/ _ \| | / _` / __| __/ __|
|
36
|
+
| | \ \ __/ |_| | | (_) | |___| (_| \__ \ |_\__ \
|
37
|
+
|_| \_\___|\__|_| \___/ \_____\__,_|___/\__|___/
|
38
|
+
Welcome to RetroCasts!
|
39
|
+
##################################################
|
40
|
+
1. Foundation - Jun 16, 2013
|
41
|
+
2. Form Objects - Jun 3, 2013
|
42
|
+
3. Model Caching (revised) - May 13, 2013
|
43
|
+
4. Upgrading to Rails 4 - May 6, 2013
|
44
|
+
5. Batch API Requests - Apr 27, 2013
|
45
|
+
6. Handling Exceptions (revised) - Apr 20, 2013
|
46
|
+
7. Fast Tests - Apr 10, 2013
|
47
|
+
8. Fast Rails Commands - Apr 4, 2013
|
48
|
+
9. Performance Testing - Mar 27, 2013
|
49
|
+
10. Eager Loading (revised) - Mar 20, 2013
|
50
|
+
Please select an option...
|
51
|
+
Episodes: 1 to 10 | home | search {search terms} | next | back | exit
|
52
|
+
>
|
53
|
+
```
|
54
|
+
In the background, RetroCasts is scraping the RailsCasts website. Therefore, the results shown above represent the episode results shown on the homepage of RailsCasts. From here several options are available.
|
55
|
+
|
56
|
+
To view an episode's details chose an episode number.
|
57
|
+
|
58
|
+
```
|
59
|
+
>1
|
60
|
+
Title: Foundation
|
61
|
+
Number: 417
|
62
|
+
Date: Jun 16, 2013
|
63
|
+
Length: 11 minutes
|
64
|
+
Description: ZURB's Foundation is a front-end for quickly building
|
65
|
+
applications and prototypes. It is similar to Twitter Bootstrap but
|
66
|
+
uses Sass instead of LESS. Here you will learn the basics of the grid
|
67
|
+
system, navigation, tooltips and more.
|
68
|
+
Link: /episodes/417-foundation
|
69
|
+
Type 'back' to go back or 'open' to open the episode in your browser.
|
70
|
+
>
|
71
|
+
```
|
72
|
+
From the detail screen, ```back``` will return to the main menu and ```open``` will open the episode in your browser.
|
73
|
+
|
74
|
+
Back on the episode menu, other options include...
|
75
|
+
|
76
|
+
* ```home``` - Go back to the RailsCasts homepage results.
|
77
|
+
* ```next``` - Go to the next page of episode results.
|
78
|
+
* ```back``` - Go to the previous page of episode results.
|
79
|
+
* ```search {search terms}``` - Search RailsCasts. There is no need to enter the search terms in quotes. Something like ```search model caching``` will work fine. the ```next``` and ```back``` commands move through search results until you go back to ```home```.
|
80
|
+
* ```exit``` - Closes the program.
|
81
|
+
|
82
|
+
### Command Line Search
|
83
|
+
RetroCasts also lets you search from the command line. For example...
|
84
|
+
|
85
|
+
$ retrocasts model caching
|
86
|
+
|
87
|
+
...will run a search instead of opening the home page results.
|
88
|
+
|
89
|
+
```
|
90
|
+
$ retrocasts model caching
|
91
|
+
_____ _ _____ _
|
92
|
+
| __ \ | | / ____| | |
|
93
|
+
| |__) |___| |_ _ __ ___ | | __ _ ___| |_ ___
|
94
|
+
| _ // _ \ __| '__/ _ \| | / _` / __| __/ __|
|
95
|
+
| | \ \ __/ |_| | | (_) | |___| (_| \__ \ |_\__ \
|
96
|
+
|_| \_\___|\__|_| \___/ \_____\__,_|___/\__|___/
|
97
|
+
Welcome to RetroCasts!
|
98
|
+
##################################################
|
99
|
+
1. Model Caching (revised) - May 13, 2013
|
100
|
+
2. Facebook Authentication - Jun 25, 2012
|
101
|
+
3. What's New in Rails 4 - Jan 4, 2013
|
102
|
+
4. Caching in Rails 2.1 - Jun 23, 2008
|
103
|
+
Please select an option...
|
104
|
+
Episodes: 1 to 4 | home | search {search terms} | next | back | exit
|
105
|
+
>
|
106
|
+
```
|
107
|
+
|
108
|
+
## Development
|
109
|
+
|
110
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
111
|
+
|
112
|
+
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).
|
113
|
+
|
114
|
+
## Contributing
|
115
|
+
|
116
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/snsavage/retro_casts](https://github.com/snsavage/retro_casts). 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.
|
117
|
+
|
118
|
+
|
119
|
+
## License
|
120
|
+
|
121
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
122
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "retro_casts"
|
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/setup
ADDED
data/exe/retrocasts
ADDED
data/lib/retro_casts.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require "open-uri"
|
3
|
+
|
4
|
+
require "retro_casts/version"
|
5
|
+
require "retro_casts/rails_casts"
|
6
|
+
require "retro_casts/episode"
|
7
|
+
require "retro_casts/website"
|
8
|
+
require "retro_casts/null_website"
|
9
|
+
require "retro_casts/CLI"
|
10
|
+
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
module RetroCasts
|
14
|
+
extend CLI
|
15
|
+
|
16
|
+
def self.start(klass: RetroCasts::RailsCasts)
|
17
|
+
retro_welcome
|
18
|
+
welcome
|
19
|
+
|
20
|
+
if !ARGV.empty?
|
21
|
+
site = klass.new(search: ARGV.join(" "))
|
22
|
+
else
|
23
|
+
site ||= klass.new
|
24
|
+
end
|
25
|
+
|
26
|
+
message = ""
|
27
|
+
|
28
|
+
loop do
|
29
|
+
display("#" * 50)
|
30
|
+
site.list_episodes
|
31
|
+
|
32
|
+
puts "*** #{message} ***" unless message == ""
|
33
|
+
message = ""
|
34
|
+
|
35
|
+
puts "Please select an option..."
|
36
|
+
puts "Episodes: 1 to #{site.episodes.length} | home | search {search terms} | next | back | exit"
|
37
|
+
print ">"
|
38
|
+
input = $stdin.gets.chomp.split(" ")
|
39
|
+
command = input.shift
|
40
|
+
argument = input.join(" ")
|
41
|
+
|
42
|
+
if integer?(command) && site.episode?(command.to_i)
|
43
|
+
loop do
|
44
|
+
episode = site.episode(command.to_i)
|
45
|
+
site.show_episode_detail(command.to_i)
|
46
|
+
|
47
|
+
puts "Type 'back' to go back or 'open' to open the episode in your browser."
|
48
|
+
print ">"
|
49
|
+
|
50
|
+
case $stdin.gets.chomp.downcase
|
51
|
+
when "back"
|
52
|
+
break
|
53
|
+
when "open"
|
54
|
+
`open #{site.host}/#{episode.link}`
|
55
|
+
else
|
56
|
+
puts "Please choose 'exit' or 'open'."
|
57
|
+
print ">"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
elsif command == nil
|
61
|
+
message = "Enter is not a valid selection."
|
62
|
+
else
|
63
|
+
case command.downcase
|
64
|
+
when "home"
|
65
|
+
puts "Going back to the homepage..."
|
66
|
+
site = site.get_search(nil)
|
67
|
+
when "search"
|
68
|
+
puts "Searching for \"#{argument}\"..."
|
69
|
+
site = site.get_search(argument)
|
70
|
+
when "next"
|
71
|
+
puts "Opening page #{site.page + 1}..."
|
72
|
+
site = site.next_page
|
73
|
+
when "back"
|
74
|
+
puts "Opening page #{site.page - 1}..."
|
75
|
+
site = site.prev_page
|
76
|
+
when "exit"
|
77
|
+
break
|
78
|
+
else
|
79
|
+
message = "#{command} is not a valid selection."
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def self.integer?(number)
|
87
|
+
begin
|
88
|
+
Integer(number)
|
89
|
+
true
|
90
|
+
rescue
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RetroCasts
|
2
|
+
module CLI
|
3
|
+
def welcome
|
4
|
+
puts "Welcome to RetroCasts!"
|
5
|
+
end
|
6
|
+
|
7
|
+
def display(message = "")
|
8
|
+
puts word_wrap(message, line_width: 70)
|
9
|
+
end
|
10
|
+
|
11
|
+
def retro_welcome
|
12
|
+
retro_welcome = <<-ASCII
|
13
|
+
_____ _ _____ _
|
14
|
+
| __ \\ | | / ____| | |
|
15
|
+
| |__) |___| |_ _ __ ___ | | __ _ ___| |_ ___
|
16
|
+
| _ // _ \\ __| '__/ _ \\| | / _` / __| __/ __|
|
17
|
+
| | \\ \\ __/ |_| | | (_) | |___| (_| \\__ \\ |_\\__ \\
|
18
|
+
|_| \\_\\___|\\__|_| \\___/ \\_____\\__,_|___/\\__|___/
|
19
|
+
ASCII
|
20
|
+
puts retro_welcome
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
# Source:
|
25
|
+
# http://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap
|
26
|
+
def word_wrap(text, line_width: 80, break_sequence: "\n")
|
27
|
+
text.split("\n").collect! do |line|
|
28
|
+
line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
|
29
|
+
end * break_sequence
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module RetroCasts
|
2
|
+
class RailsCasts
|
3
|
+
attr_reader :host, :filter, :episodes, :page, :search, :url, :website
|
4
|
+
|
5
|
+
def initialize(host: 'http://www.railscasts.com',
|
6
|
+
filter: '.episode',
|
7
|
+
page: 1,
|
8
|
+
search: nil,
|
9
|
+
website: RetroCasts::Website.new)
|
10
|
+
|
11
|
+
@host = host
|
12
|
+
@filter = filter
|
13
|
+
@page = page
|
14
|
+
@search = search
|
15
|
+
@website = website
|
16
|
+
|
17
|
+
@url = build_url
|
18
|
+
|
19
|
+
nodeset = website.get_list(url, filter)
|
20
|
+
@episodes = parse_episodes(nodeset)
|
21
|
+
end
|
22
|
+
|
23
|
+
def episode?(number)
|
24
|
+
number > 0 && number <= episodes.length
|
25
|
+
end
|
26
|
+
|
27
|
+
def episode(number)
|
28
|
+
if episode?(number)
|
29
|
+
episodes[number - 1]
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_search(search_term)
|
36
|
+
self.class.new(search: search_term, website: website)
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_page
|
40
|
+
self.class.new(search: search,
|
41
|
+
page: page + 1,
|
42
|
+
website: website)
|
43
|
+
end
|
44
|
+
|
45
|
+
def prev_page
|
46
|
+
if page > 1
|
47
|
+
self.class.new(search: search,
|
48
|
+
page: page - 1,
|
49
|
+
website: website)
|
50
|
+
else
|
51
|
+
self
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def list_episodes
|
56
|
+
episodes.each_with_index do |episode, i|
|
57
|
+
RetroCasts.display "#{i +1}. #{episode.title} - #{episode.date}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def show_episode_detail(episode_number)
|
62
|
+
current_episode = episode(episode_number)
|
63
|
+
[:title, :number, :date, :length, :description, :link].each do |attribute|
|
64
|
+
label = attribute.to_s.capitalize
|
65
|
+
message = "#{label}: #{current_episode.send(attribute)}"
|
66
|
+
RetroCasts.display(message)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def build_url
|
72
|
+
attributes = {}
|
73
|
+
|
74
|
+
attributes[:search] = search if search
|
75
|
+
attributes[:page] = page if page != 1
|
76
|
+
|
77
|
+
if !attributes.empty?
|
78
|
+
query = URI.encode_www_form(attributes)
|
79
|
+
end
|
80
|
+
|
81
|
+
if attributes.has_key?(:search)
|
82
|
+
"#{host}\/episodes?#{query}"
|
83
|
+
elsif attributes.has_key?(:page)
|
84
|
+
"#{host}\/?#{query}"
|
85
|
+
elsif host != ""
|
86
|
+
"#{host}\/"
|
87
|
+
else
|
88
|
+
""
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_episodes(nodeset)
|
93
|
+
nodeset.collect do |node|
|
94
|
+
create_episode(node)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_episode(node)
|
99
|
+
episode = Episode.new
|
100
|
+
episode.title = title(node)
|
101
|
+
episode.number = number(node)
|
102
|
+
episode.date = date(node)
|
103
|
+
episode.length = length(node)
|
104
|
+
episode.link = link(node)
|
105
|
+
episode.description = description(node)
|
106
|
+
return episode
|
107
|
+
end
|
108
|
+
|
109
|
+
def title(node)
|
110
|
+
node.css(".main h2 a").text
|
111
|
+
end
|
112
|
+
|
113
|
+
def number(node)
|
114
|
+
text = node.css(".number").text
|
115
|
+
text.match(/\d+/).to_s.to_i
|
116
|
+
end
|
117
|
+
|
118
|
+
def date(node)
|
119
|
+
text = node.css(".published_at").text
|
120
|
+
Date.parse(text)
|
121
|
+
end
|
122
|
+
|
123
|
+
def length(node)
|
124
|
+
text = node.css(".stats").text
|
125
|
+
text.match(/\d+/).to_s.to_i
|
126
|
+
end
|
127
|
+
|
128
|
+
def link(node)
|
129
|
+
node.css(".screenshot a").first.attributes["href"].value
|
130
|
+
end
|
131
|
+
|
132
|
+
def description(node)
|
133
|
+
text = node.css(".description").text
|
134
|
+
text.sub(/\(\d+ minutes\)/, '').delete("\n").strip
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|