├── .yardopts ├── .gitignore ├── .rspec ├── config └── urls.yml ├── lib ├── elixir_sips_downloader │ ├── extractor.rb │ ├── downloadable.rb │ ├── config.rb │ ├── extractors.rb │ ├── downloadables.rb │ ├── extractors │ │ ├── files.rb │ │ ├── episode.rb │ │ └── catalog.rb │ ├── feed_fetcher.rb │ ├── downloadables │ │ ├── catalog.rb │ │ ├── file.rb │ │ └── episode.rb │ ├── login.rb │ └── cli.rb └── elixir-sips-downloader.rb ├── Gemfile ├── bin └── elixir-sips-downloader ├── spec ├── elixir_sips_downloader │ ├── extractor_spec.rb │ ├── downloadable_spec.rb │ ├── feed_fetcher_spec.rb │ ├── login_spec.rb │ ├── extractors │ │ ├── files_spec.rb │ │ ├── episode_spec.rb │ │ └── catalog_spec.rb │ └── downloadables │ │ ├── catalog_spec.rb │ │ ├── file_spec.rb │ │ └── episode_spec.rb ├── spec_helper.rb └── fixtures │ ├── feed_description.html │ ├── feed_description_with_list.html │ └── feed.xml ├── README.md └── Gemfile.lock /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | videos/ 2 | *.DS_Store 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /config/urls.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :login: https://elixirsips.dpdcart.com/subscriber/content 3 | :feed: https://elixirsips.dpdcart.com/feed 4 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/extractor.rb: -------------------------------------------------------------------------------- 1 | # The contract for Extractors. 2 | class ElixirSipsDownloader::Extractor 3 | # Should be implemented by children. 4 | def extract 5 | fail NotImplementedError 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/downloadable.rb: -------------------------------------------------------------------------------- 1 | # The contract for Downloadables. 2 | class ElixirSipsDownloader::Downloadable 3 | # Should be implemented by children. 4 | def download 5 | fail NotImplementedError 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'mechanize' 4 | gem 'mechanize-progressbar', git: 'https://github.com/Animagani/Mechanize-ProgressBar.git' 5 | 6 | group :test do 7 | gem 'debugger', '~> 1.6.5' 8 | gem 'rspec' 9 | gem 'pry-debugger' 10 | end 11 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/config.rb: -------------------------------------------------------------------------------- 1 | # Retrieve configurations. 2 | class ElixirSipsDownloader::Config 3 | # Retrieve urls stored in `urls.yml`. 4 | # @return [Hash] the urls stored in `urls.yml`. 5 | def self.urls 6 | @urls ||= YAML.load File.read 'config/urls.yml' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/extractors.rb: -------------------------------------------------------------------------------- 1 | # Namespace module for Extractors. 2 | module ElixirSipsDownloader::Extractors 3 | end 4 | 5 | require_relative 'extractor' 6 | 7 | require_relative 'extractors/files' 8 | require_relative 'extractors/episode' 9 | require_relative 'extractors/catalog' 10 | -------------------------------------------------------------------------------- /bin/elixir-sips-downloader: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if ARGV.count != 3 4 | abort <<-USAGE 5 | Usage: 6 | $ elixir-sips-downloader 7 | USAGE 8 | end 9 | 10 | require_relative '../lib/elixir_sips_downloader/cli' 11 | 12 | ElixirSipsDownloader::CLI.new(*ARGV).download 13 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/downloadables.rb: -------------------------------------------------------------------------------- 1 | # Namespace module for Downloadable assets. 2 | module ElixirSipsDownloader::Downloadables 3 | end 4 | 5 | require_relative 'downloadable' 6 | 7 | require_relative 'downloadables/file' 8 | require_relative 'downloadables/episode' 9 | require_relative 'downloadables/catalog' 10 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/extractor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Extractor do 4 | subject(:extractor) { extractor_class.new } 5 | 6 | let(:extractor_class) { Class.new ElixirSipsDownloader::Extractor } 7 | 8 | describe 'contract' do 9 | specify('#extract') { 10 | expect { extractor.extract }.to raise_error NotImplementedError 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/elixir-sips-downloader' 2 | 3 | ElixirSipsDownloader.logger = Object.new.tap do |logger| 4 | def logger.method_missing(*) 5 | end 6 | end 7 | 8 | RSpec.configure do |config| 9 | config.treat_symbols_as_metadata_keys_with_true_values = true 10 | config.run_all_when_everything_filtered = true 11 | config.filter_run :focus 12 | config.order = 'random' 13 | end 14 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/downloadable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Downloadable do 4 | subject(:downloadable) { downloadable_class.new } 5 | 6 | let(:downloadable_class) { Class.new ElixirSipsDownloader::Downloadable } 7 | 8 | describe 'contract' do 9 | specify('#download') { 10 | expect { downloadable.download }.to raise_error NotImplementedError 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/feed_description.html: -------------------------------------------------------------------------------- 1 |
2 |

Short description of episode

3 |
4 |

Attached Files

5 | 10 |
11 | -------------------------------------------------------------------------------- /spec/fixtures/feed_description_with_list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Short description of episode

4 | 9 |
10 |

Attached Files

11 | 16 |
17 | -------------------------------------------------------------------------------- /lib/elixir-sips-downloader.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'fileutils' 3 | require 'rss' 4 | require 'rexml/document' 5 | require 'set' 6 | require 'cgi' 7 | require 'logger' 8 | 9 | module ElixirSipsDownloader 10 | class << self 11 | # The Logger for ElixirSipsDownloader. 12 | attr_writer :logger 13 | 14 | # @return [Logger] the Logger for ElixirSipsDownloader 15 | def logger 16 | @logger ||= Logger.new STDOUT 17 | end 18 | end 19 | end 20 | 21 | require 'bundler/setup' 22 | require 'mechanize' 23 | 24 | require_relative 'elixir_sips_downloader/downloadables' 25 | 26 | require_relative 'elixir_sips_downloader/extractors' 27 | 28 | require_relative 'elixir_sips_downloader/config' 29 | require_relative 'elixir_sips_downloader/login' 30 | require_relative 'elixir_sips_downloader/feed_fetcher' 31 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/extractors/files.rb: -------------------------------------------------------------------------------- 1 | # Extract a Set of Files from an Feed Item Description. 2 | class ElixirSipsDownloader::Extractors::Files < ElixirSipsDownloader::Extractor 3 | # @param item_description [String] the feed item description extracted with 4 | # `feed.items[i].description`. 5 | # @return [Set] the Set of Files 6 | # extracted from feed item description. 7 | def extract item_description 8 | files = Set.new 9 | document = REXML::Document.new item_description 10 | document.elements.each("/div[@class='blog-entry']/ul/li/a") { |element| 11 | name = element.text 12 | link = element.attribute('href').to_s 13 | files << ElixirSipsDownloader::Downloadables::File.new(name, link) 14 | } 15 | files 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/extractors/episode.rb: -------------------------------------------------------------------------------- 1 | # Extract an Episode from an Feed Item. 2 | class ElixirSipsDownloader::Extractors::Episode < ElixirSipsDownloader::Extractor 3 | # @param files_extractor [ElixirSipsDownloader::Extractors::Files] the 4 | # Files Extractor. 5 | def initialize files_extractor = ElixirSipsDownloader::Extractors::Files.new 6 | @files_extractor = files_extractor 7 | end 8 | 9 | # @param item [RSS::Rss::Channel::Item] the feed item extracted with 10 | # `feed.items[i]`. 11 | # @return [ElixirSipsDownloader::Downloadables::Episode] the Episode extracted 12 | # from feed item. 13 | def extract item 14 | title = CGI.unescapeHTML item.title 15 | link = item.link 16 | files = @files_extractor.extract item.description 17 | 18 | ElixirSipsDownloader::Downloadables::Episode.new title, link, files 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/feed_fetcher.rb: -------------------------------------------------------------------------------- 1 | # Fetches feed from Ruby Tapas. 2 | class ElixirSipsDownloader::FeedFetcher 3 | # @return [Mechanize] the Mechanize agent. 4 | attr_reader :agent 5 | 6 | # @return [String] the e-mail for the user. 7 | attr_reader :email 8 | 9 | # @return [String] the password for the user. 10 | attr_reader :password 11 | 12 | def initialize agent, email, password 13 | @agent = agent 14 | @email = email 15 | @password = password 16 | end 17 | 18 | # Fetch feed from Ruby Tapas. 19 | # 20 | # @return [RSS::Rss] the feed for Ruby Tapas. 21 | def fetch 22 | ElixirSipsDownloader.logger.info 'Fetching episodes feed...' 23 | 24 | feed_url = ElixirSipsDownloader::Config.urls[:feed] 25 | 26 | agent.add_auth feed_url, email, password 27 | RSS::Parser.parse agent.get(feed_url).body 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/extractors/catalog.rb: -------------------------------------------------------------------------------- 1 | # Extract an Catalog from an Feed. 2 | class ElixirSipsDownloader::Extractors::Catalog < ElixirSipsDownloader::Extractor 3 | # @param episode_extractor [ElixirSipsDownloader::Extractors::Episode] the 4 | # Episode Extractor. 5 | def initialize episode_extractor = 6 | ElixirSipsDownloader::Extractors::Episode.new 7 | @episode_extractor = episode_extractor 8 | end 9 | 10 | # @param feed [RSS::Rss] the feed extracted with `RSS::Parser.parse`. 11 | # @return [ElixirSipsDownloader::Downloadables::Catalog] the Catalog extracted 12 | # from feed. 13 | def extract feed 14 | episodes = Set.new 15 | 16 | feed.items.each { |item| 17 | episodes << @episode_extractor.extract(item) 18 | } 19 | 20 | ElixirSipsDownloader::Downloadables::Catalog.new episodes 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/downloadables/catalog.rb: -------------------------------------------------------------------------------- 1 | # Catalog is the set of all Ruby Tapas Episodes. 2 | class ElixirSipsDownloader::Downloadables::Catalog < 3 | ElixirSipsDownloader::Downloadable 4 | 5 | # @return [Set] the Episodes. 6 | attr_reader :episodes 7 | 8 | def initialize episodes 9 | @episodes = episodes 10 | end 11 | 12 | # Download the Catalog. 13 | # 14 | # @param basepath [String] the path to place download. 15 | # @param agent [Mechanize] the Mechanize agent. 16 | def download basepath, agent 17 | ElixirSipsDownloader.logger.info 'Starting download of catalog in ' \ 18 | "`#{ basepath }'..." 19 | FileUtils.mkdir_p basepath 20 | episodes.each { |episode| episode.download basepath, agent } 21 | end 22 | 23 | def == other 24 | episodes == other.episodes 25 | end 26 | 27 | def eql? other 28 | episodes.eql? other.episodes 29 | end 30 | 31 | def hash 32 | episodes.hash 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/login.rb: -------------------------------------------------------------------------------- 1 | # Perform Login in Ruby Tapas. 2 | # 3 | # Login must be performed before any attempt to download files. 4 | class ElixirSipsDownloader::Login 5 | # @return [Mechanize] the Mechanize agent. 6 | attr_reader :agent 7 | 8 | # @return [String] the e-mail for the user. 9 | attr_reader :email 10 | 11 | # @return [String] the password for the user. 12 | attr_reader :password 13 | 14 | def initialize agent, email, password 15 | @agent = agent 16 | @email = email 17 | @password = password 18 | end 19 | 20 | # Perform login. 21 | def login 22 | ElixirSipsDownloader.logger.info 'Logging in...' 23 | request_login_page 24 | fill_login_form 25 | submit_login_form 26 | end 27 | 28 | private 29 | 30 | def request_login_page 31 | @page = agent.get ElixirSipsDownloader::Config.urls.fetch(:login) 32 | end 33 | 34 | def fill_login_form 35 | login_form.username = email 36 | login_form.password = password 37 | end 38 | 39 | def submit_login_form 40 | login_form.submit 41 | end 42 | 43 | def login_form 44 | @page.forms.first 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/feed_fetcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::FeedFetcher do 4 | subject(:feed_fetcher) { 5 | ElixirSipsDownloader::FeedFetcher.new agent, email, password 6 | } 7 | 8 | let(:agent) { double } 9 | let(:email) { 'person@example.com' } 10 | let(:password) { 'chuncky bacon' } 11 | 12 | describe '#fetch' do 13 | subject(:fetched_feed) { feed_fetcher.fetch } 14 | 15 | let(:feed_string) { double } 16 | let(:feed) { double } 17 | 18 | it 'fetches feed' do 19 | expect(agent).to receive(:add_auth) 20 | .with( 21 | ElixirSipsDownloader::Config.urls[:feed], 22 | email, 23 | password 24 | ) 25 | 26 | expect(agent).to receive(:get) 27 | .with(ElixirSipsDownloader::Config.urls[:feed]) 28 | .and_return(double body: feed_string) 29 | 30 | expect(RSS::Parser).to receive(:parse).with(feed_string).and_return(feed) 31 | 32 | expect(fetched_feed).to eq(feed) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/login_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Login do 4 | subject(:elixir_sips_downloader) { 5 | ElixirSipsDownloader::Login.new agent, email, password 6 | } 7 | 8 | let(:agent) { double get: page } 9 | let(:email) { 'someone@example.com' } 10 | let(:password) { 'chunky bacon' } 11 | 12 | let(:page) { double forms: [login_form] } 13 | let(:login_form) { 14 | double :username= => nil, 15 | :password= => nil, 16 | :submit => nil 17 | } 18 | 19 | describe '#login' do 20 | it 'requests the login page' do 21 | expect(agent).to receive(:get) 22 | .with(ElixirSipsDownloader::Config.urls.fetch(:login)) 23 | elixir_sips_downloader.login 24 | end 25 | 26 | it 'fills in the login form' do 27 | expect(login_form).to receive(:username=).with(email) 28 | expect(login_form).to receive(:password=).with(password) 29 | elixir_sips_downloader.login 30 | end 31 | 32 | it 'submits the login form' do 33 | expect(login_form).to receive(:submit) 34 | elixir_sips_downloader.login 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/downloadables/file.rb: -------------------------------------------------------------------------------- 1 | require 'mechanize/progressbar' 2 | 3 | # The File resource of an Episode. 4 | class ElixirSipsDownloader::Downloadables::File < 5 | ElixirSipsDownloader::Downloadable 6 | 7 | # @return [String] the name of the File. 8 | attr_reader :name 9 | 10 | # @return [String] the link to download the File. 11 | attr_reader :link 12 | 13 | def initialize name, link 14 | @name = name 15 | @link = link 16 | end 17 | 18 | # Download the File. 19 | # 20 | # @param (see: ElixirSipsDownloader::Downloadables::Catalog#download) 21 | def download basepath, agent 22 | FileUtils.mkdir_p basepath 23 | 24 | file_path = File.join(basepath, name) 25 | ElixirSipsDownloader.logger.info "Starting download of file `#{ name }' " \ 26 | "in `#{ file_path }'..." 27 | unless File.exists? file_path 28 | agent.progressbar { 29 | agent.download link, file_path 30 | } 31 | end 32 | end 33 | 34 | def == other 35 | name == other.name && link == other.link 36 | end 37 | 38 | def eql? other 39 | name.eql?(other.name) && link.eql?(other.link) 40 | end 41 | 42 | def hash 43 | name.hash + link.hash 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/cli.rb: -------------------------------------------------------------------------------- 1 | require_relative '../elixir-sips-downloader' 2 | 3 | # The Command Line Interface for Ruby Tapas Downloader. 4 | class ElixirSipsDownloader::CLI 5 | # @param email [String] the e-mail for the user. 6 | # @param password [String] the password for the user. 7 | # @param download_path [String] the path in which the download is performed. 8 | def initialize email, password, download_path 9 | @email = email 10 | @password = password 11 | @download_path = download_path 12 | end 13 | 14 | # Perform complete download procedure. 15 | def download 16 | create_agent 17 | login 18 | fetch_feed 19 | create_catalog 20 | download_catalog 21 | end 22 | 23 | private 24 | 25 | def create_agent 26 | @agent = Mechanize.new 27 | end 28 | 29 | def login 30 | ElixirSipsDownloader::Login.new(@agent, @email, @password).login 31 | end 32 | 33 | def fetch_feed 34 | @feed = ElixirSipsDownloader::FeedFetcher.new(@agent, @email, @password) 35 | .fetch 36 | end 37 | 38 | def create_catalog 39 | @catalog = ElixirSipsDownloader::Extractors::Catalog.new.extract @feed 40 | end 41 | 42 | def download_catalog 43 | @catalog.download @download_path, @agent 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/extractors/files_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Extractors::Files do 4 | subject(:files_extractor) { 5 | ElixirSipsDownloader::Extractors::Files.new 6 | } 7 | 8 | it 'is an Extractor' do 9 | expect(files_extractor).to be_a ElixirSipsDownloader::Extractor 10 | end 11 | 12 | describe '#extract' do 13 | subject(:extracted_files) { files_extractor.extract item_description } 14 | 15 | let(:item_description) { File.read 'spec/fixtures/feed_description.html' } 16 | let(:files) { 17 | Set[ 18 | ElixirSipsDownloader::Downloadables::File.new( 19 | 'some-episode-file.html', 20 | 'http://example.com/some-episode-file.html'), 21 | ElixirSipsDownloader::Downloadables::File.new( 22 | 'some-episode-file.mp4', 23 | 'http://example.com/some-episode-file.mp4'), 24 | ElixirSipsDownloader::Downloadables::File.new( 25 | 'some-episode-file.rb', 26 | 'http://example.com/some-episode-file.rb'), 27 | ] 28 | } 29 | 30 | it 'returns a Set of Files' do 31 | expect(extracted_files).to eq(files) 32 | end 33 | 34 | context 'list of links in description that are not attached files' do 35 | let(:item_description) { 36 | File.read('spec/fixtures/feed_description_with_list.html') 37 | } 38 | 39 | it 'ignores the list' do 40 | expect(extracted_files).to eq(files) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/elixir_sips_downloader/downloadables/episode.rb: -------------------------------------------------------------------------------- 1 | # An Ruby Tapas Episode. 2 | class ElixirSipsDownloader::Downloadables::Episode < 3 | ElixirSipsDownloader::Downloadable 4 | 5 | # @return [String] the title of the Episode. 6 | attr_reader :title 7 | 8 | # @return [String] the link to the Episode. 9 | attr_reader :link 10 | 11 | # @return [Set] the Set of Files 12 | # for that episode. 13 | attr_reader :files 14 | 15 | def initialize title, link, files 16 | @title = title 17 | @link = link 18 | @files = files 19 | end 20 | 21 | # Clean title to be used in path names. 22 | # 23 | # @return [String] the sanitized title. 24 | def sanitized_title 25 | @sanitized_title ||= title.downcase.gsub(/[^\w<>#?!$]+/, '-') 26 | end 27 | 28 | 29 | # Download the Episode. 30 | # 31 | # @param (see: ElixirSipsDownloader::Downloadables::Catalog#download) 32 | def download basepath, agent 33 | episode_path = File.join basepath, sanitized_title 34 | ElixirSipsDownloader.logger.info 'Starting download of episode ' \ 35 | "`#{ title }' in `#{ episode_path }'..." 36 | FileUtils.mkdir_p episode_path 37 | files.each { |file| file.download episode_path, agent } 38 | end 39 | 40 | def == other 41 | title == other.title && link == other.link && files == other.files 42 | end 43 | 44 | def eql? other 45 | title.eql?(other.title) && link.eql?(other.link) && files.eql?(other.files) 46 | end 47 | 48 | def hash 49 | title.hash + link.hash + files.hash 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/downloadables/catalog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Downloadables::Catalog do 4 | subject(:catalog) { 5 | ElixirSipsDownloader::Downloadables::Catalog.new episodes 6 | } 7 | 8 | let(:episodes) { [double(download: true), double(download: true)] } 9 | 10 | specify('#episodes') { expect(catalog.episodes).to eq(episodes) } 11 | 12 | it 'is downloadable' do 13 | expect(catalog).to be_a ElixirSipsDownloader::Downloadable 14 | end 15 | 16 | describe '#download' do 17 | let(:basepath) { '/tmp/ruby-tapas' } 18 | let(:agent) { double } 19 | 20 | before { allow(FileUtils).to receive(:mkdir_p) } 21 | 22 | it 'creates folder for catalog' do 23 | expect(FileUtils).to receive(:mkdir_p).with(basepath) 24 | 25 | catalog.download basepath, agent 26 | end 27 | 28 | it 'calls #download on each episode' do 29 | episodes.each do |episode| 30 | expect(episode).to receive(:download).with(basepath, agent) 31 | end 32 | 33 | catalog.download basepath, agent 34 | end 35 | end 36 | 37 | describe '#==' do 38 | it 'compares episodes' do 39 | expect(catalog).to eq( 40 | ElixirSipsDownloader::Downloadables::Catalog.new episodes) 41 | end 42 | end 43 | 44 | describe '#eql?' do 45 | it 'compares episodes' do 46 | expect(catalog.eql?( 47 | ElixirSipsDownloader::Downloadables::Catalog.new episodes)).to be_true 48 | end 49 | end 50 | 51 | describe '#hash' do 52 | it 'is based on episodes' do 53 | expect(catalog.hash).to eq(episodes.hash) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/extractors/episode_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Extractors::Episode do 4 | subject(:episode_extractor) { 5 | ElixirSipsDownloader::Extractors::Episode.new files_extractor 6 | } 7 | 8 | let(:files_extractor) { double(extract: files) } 9 | let(:files) { 10 | Set[ 11 | ElixirSipsDownloader::Downloadables::File.new( 12 | 'some-episode-file.html', 13 | 'http://example.com/some-episode-file.html'), 14 | ElixirSipsDownloader::Downloadables::File.new( 15 | 'some-episode-file.mp4', 16 | 'http://example.com/some-episode-file.mp4'), 17 | ElixirSipsDownloader::Downloadables::File.new( 18 | 'some-episode-file.rb', 19 | 'http://example.com/some-episode-file.rb'), 20 | ] 21 | } 22 | 23 | it 'is an Extractor' do 24 | expect(episode_extractor).to be_a ElixirSipsDownloader::Extractor 25 | end 26 | 27 | describe '#extract' do 28 | subject(:episode) { episode_extractor.extract item } 29 | 30 | let(:item) { 31 | RSS::Parser.parse(File.open('spec/fixtures/feed.xml')).items.first 32 | } 33 | 34 | it 'uses Extractors::Files' do 35 | expect(files_extractor).to receive(:extract).with(item.description) 36 | .and_return(files) 37 | episode_extractor.extract item 38 | end 39 | 40 | it 'returns an Episode' do 41 | expect(episode).to eq( 42 | ElixirSipsDownloader::Downloadables::Episode.new( 43 | '129 Some episode', 44 | 'http://example.com/some-episode', 45 | files 46 | ) 47 | ) 48 | end 49 | 50 | it 'decodes HTML entities in title' do 51 | item.title = '120 class << self' 52 | 53 | expect(episode.title).to eq('120 class << self') 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/downloadables/file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Downloadables::File do 4 | subject(:file) { ElixirSipsDownloader::Downloadables::File.new name, link } 5 | 6 | let(:name) { 'an-awesome-screencast.mp4' } 7 | let(:link) { 'http://example.com/an-awesome-screencast.mp4'} 8 | 9 | specify('#name') { expect(file.name).to eq(name) } 10 | specify('#link') { expect(file.link).to eq(link) } 11 | 12 | it 'is downloadable' do 13 | expect(file).to be_a ElixirSipsDownloader::Downloadable 14 | end 15 | 16 | describe '#download' do 17 | let(:basepath) { '/tmp/ruby-tapas/some-episode' } 18 | let(:agent) { double(download: true) } 19 | let(:file_path) { File.join basepath, name } 20 | 21 | before { allow(FileUtils).to receive(:mkdir_p) } 22 | 23 | it 'creates folder for file' do 24 | expect(FileUtils).to receive(:mkdir_p).with(basepath) 25 | 26 | file.download basepath, agent 27 | end 28 | 29 | it 'downloads the file' do 30 | expect(agent).to receive(:download).with(link, file_path) 31 | 32 | file.download basepath, agent 33 | end 34 | 35 | it 'avoids repeating download' do 36 | allow(File).to receive(:exists?).with(file_path).and_return(true) 37 | expect(agent).to_not receive(:download) 38 | 39 | file.download basepath, agent 40 | end 41 | end 42 | 43 | describe '#==' do 44 | it 'compares name and link' do 45 | expect(file).to eq( 46 | ElixirSipsDownloader::Downloadables::File.new(name, link)) 47 | end 48 | end 49 | 50 | describe '#eql?' do 51 | it 'compares name and link' do 52 | expect( 53 | file.eql? ElixirSipsDownloader::Downloadables::File.new(name, link) 54 | ).to be_true 55 | end 56 | end 57 | 58 | describe '#hash' do 59 | it 'is based on name and link' do 60 | expect(file.hash).to eq(name.hash + link.hash) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Elixir Sips Downloader 2 | ====================== 3 | 4 | Elixir Sips Downloader downloads all episodes and attachments, 5 | organizes them for later use and keeps an easy to use index of episodes. 6 | 7 | This is just a simple modification of [@leafac's](https://github.com/leafac) [ruby-tapas-downloader](https://github.com/leafac/ruby-tapas-downloader). 8 | 9 | All credit should go to him. :) 10 | 11 | Installation 12 | ------------ 13 | 14 | 1. Clone this repository: 15 | 16 | ```bash 17 | $ git clone https://github.com/benjamintanweihao/elixir-sips-downloader.git 18 | ``` 19 | 20 | 2. Install dependencies: 21 | 22 | ```bash 23 | $ bundle install 24 | ``` 25 | 26 | Usage 27 | ----- 28 | 29 | ```bash 30 | $ bin/elixir-sips-downloader 31 | ``` 32 | 33 | Warning 34 | ------- 35 | 36 | Except for a few episodes, Elixir Sips is a paid screencast series. Therefore, please 37 | don't redistribute the downloaded material. Elixir Sips Downloader is 38 | only an utility tool and doesn't substitute the subscription. 39 | 40 | You should do no evil! 41 | 42 | Thanks 43 | ------ 44 | 45 | Go support [Josh Adams](https://plus.google.com/104164786189701557366?rel=author) and [Elixir Sips](http://elixirsips.com/)! 46 | 47 | Also, and obviously, props to [Leandro Facchinetti](https://github.com/leafac) for making the downloading of these 48 | screencasts so much easier. 49 | 50 | License 51 | ------- 52 | 53 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 54 | Version 2, December 2004 55 | 56 | Copyright (C) 2013 Benjamin Tan Wei Hao 57 | 58 | Everyone is permitted to copy and distribute verbatim or modified 59 | copies of this license document, and changing it is allowed as long 60 | as the name is changed. 61 | 62 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 63 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 64 | 65 | 0. You just DO WHAT THE FUCK YOU WANT TO. 66 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Animagani/Mechanize-ProgressBar.git 3 | revision: fab9e9f04bc32f1cbd978a54d7693f441bbf6547 4 | specs: 5 | mechanize-progressbar (0.0.1) 6 | mechanize 7 | progressbar 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | coderay (1.1.0) 13 | columnize (0.3.6) 14 | debugger (1.6.5) 15 | columnize (>= 0.3.1) 16 | debugger-linecache (~> 1.2.0) 17 | debugger-ruby_core_source (~> 1.3.1) 18 | debugger-linecache (1.2.0) 19 | debugger-ruby_core_source (1.3.1) 20 | diff-lcs (1.2.5) 21 | domain_name (0.5.15) 22 | unf (>= 0.0.5, < 1.0.0) 23 | http-cookie (1.0.2) 24 | domain_name (~> 0.5) 25 | mechanize (2.7.3) 26 | domain_name (~> 0.5, >= 0.5.1) 27 | http-cookie (~> 1.0) 28 | mime-types (~> 2.0) 29 | net-http-digest_auth (~> 1.1, >= 1.1.1) 30 | net-http-persistent (~> 2.5, >= 2.5.2) 31 | nokogiri (~> 1.4) 32 | ntlm-http (~> 0.1, >= 0.1.1) 33 | webrobots (>= 0.0.9, < 0.2) 34 | method_source (0.8.2) 35 | mime-types (2.1) 36 | mini_portile (0.5.2) 37 | net-http-digest_auth (1.4) 38 | net-http-persistent (2.9.1) 39 | nokogiri (1.6.1) 40 | mini_portile (~> 0.5.0) 41 | ntlm-http (0.1.1) 42 | progressbar (0.21.0) 43 | pry (0.9.12.6) 44 | coderay (~> 1.0) 45 | method_source (~> 0.8) 46 | slop (~> 3.4) 47 | pry-debugger (0.2.2) 48 | debugger (~> 1.3) 49 | pry (~> 0.9.10) 50 | rspec (2.14.1) 51 | rspec-core (~> 2.14.0) 52 | rspec-expectations (~> 2.14.0) 53 | rspec-mocks (~> 2.14.0) 54 | rspec-core (2.14.7) 55 | rspec-expectations (2.14.5) 56 | diff-lcs (>= 1.1.3, < 2.0) 57 | rspec-mocks (2.14.5) 58 | slop (3.4.7) 59 | unf (0.1.3) 60 | unf_ext 61 | unf_ext (0.0.6) 62 | webrobots (0.1.1) 63 | 64 | PLATFORMS 65 | ruby 66 | 67 | DEPENDENCIES 68 | debugger (~> 1.6.5) 69 | mechanize 70 | mechanize-progressbar! 71 | pry-debugger 72 | rspec 73 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/downloadables/episode_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Downloadables::Episode do 4 | subject(:episode) { 5 | ElixirSipsDownloader::Downloadables::Episode.new title, link, files 6 | } 7 | 8 | let(:title) { '999 Some: Ruby Tapas Episode with <<' } 9 | let(:link) { 'http://example.com' } 10 | let(:files) { [double(download: true), double(download: true)] } 11 | let(:sanitized_title) { '999-some-ruby-tapas-episode-with-<<' } 12 | 13 | specify('#title') { expect(episode.title).to eq(title) } 14 | specify('#link' ) { expect(episode.link ).to eq(link ) } 15 | specify('#files') { expect(episode.files).to eq(files) } 16 | specify '#sanitized_title' do 17 | expect(episode.sanitized_title).to eq(sanitized_title) 18 | end 19 | 20 | it 'is downloadable' do 21 | expect(episode).to be_a ElixirSipsDownloader::Downloadable 22 | end 23 | 24 | describe '#download' do 25 | let(:basepath) { '/tmp/ruby-tapas' } 26 | let(:agent) { double } 27 | let(:episode_path) { File.join basepath, sanitized_title } 28 | 29 | before { allow(FileUtils).to receive(:mkdir_p) } 30 | 31 | it 'creates folder for episode with sanitized title' do 32 | expect(FileUtils).to receive(:mkdir_p).with(episode_path) 33 | 34 | episode.download basepath, agent 35 | end 36 | 37 | it 'calls #download on each file' do 38 | files.each do |file| 39 | expect(file).to receive(:download).with(episode_path, agent) 40 | end 41 | 42 | episode.download basepath, agent 43 | end 44 | end 45 | 46 | describe '#==' do 47 | it 'compares title, link and files' do 48 | expect(episode).to eq( 49 | ElixirSipsDownloader::Downloadables::Episode.new title, link, files) 50 | end 51 | end 52 | 53 | describe '#eql?' do 54 | it 'compares title, link and files' do 55 | expect( 56 | episode.eql?( 57 | ElixirSipsDownloader::Downloadables::Episode.new title, link, files) 58 | ).to be_true 59 | end 60 | end 61 | 62 | describe '#hash' do 63 | it 'is based on title, link and files' do 64 | expect(episode.hash).to eq(title.hash + link.hash + files.hash) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/fixtures/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | https://example.com 6 | 7 | Thu, 29 Aug 2013 09:00:00 -0400 8 | example@example.com 9 | en 10 | Copyright 2013 Example 11 | example.com 12 | Example: example example 13 | 14 | 15 | http://example.com/example.png 16 | Example 17 | http://example.com 18 | 849 19 | 849 20 | 21 | 22 | <![CDATA[129 Some episode]]> 23 | http://example.com/some-episode 24 | 25 |

Short description of episode

26 |
27 |

Attached Files

28 | ]]>
33 | asslaksda-sdfasd-fkasdfkas-ka-sdjga-sdfk- 34 | Thu, 29 Aug 2013 09:00:00 -0400 35 | 36 | Some subtitle 37 | 38 |
39 | 40 | <![CDATA[130 Some other episode]]> 41 | http://example.com/some-other-episode 42 | 43 |

Short description of other episode

44 |
45 |

Attached Files

46 | ]]>
51 | laksdjfasd-fasjgasfgkasdjf-asdfajdskfjadsf- 52 | Thu, 29 Aug 2013 09:00:00 -0400 53 | 54 | Some subtitle 55 | 56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /spec/elixir_sips_downloader/extractors/catalog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ElixirSipsDownloader::Extractors::Catalog do 4 | subject(:catalog_extractor) { 5 | ElixirSipsDownloader::Extractors::Catalog.new episode_extractor 6 | } 7 | 8 | let(:episode_extractor) { double } 9 | let(:episodes) { Set[first_episode, second_episode] } 10 | 11 | let(:first_episode) { 12 | ElixirSipsDownloader::Downloadables::Episode.new( 13 | '129 Some episode', 14 | 'http://example.com/some-episode', 15 | Set[ 16 | ElixirSipsDownloader::Downloadables::File.new( 17 | 'some-episode-file.html', 18 | 'http://example.com/some-episode-file.html'), 19 | ElixirSipsDownloader::Downloadables::File.new( 20 | 'some-episode-file.mp4', 21 | 'http://example.com/some-episode-file.mp4'), 22 | ElixirSipsDownloader::Downloadables::File.new( 23 | 'some-episode-file.rb', 24 | 'http://example.com/some-episode-file.rb'), 25 | ] 26 | ) 27 | } 28 | 29 | let(:second_episode) { 30 | ElixirSipsDownloader::Downloadables::Episode.new( 31 | '130 Some other episode', 32 | 'http://example.com/some-other-episode', 33 | Set[ 34 | ElixirSipsDownloader::Downloadables::File.new( 35 | 'some-other-episode-file.html', 36 | 'http://example.com/some-other-episode-file.html'), 37 | ElixirSipsDownloader::Downloadables::File.new( 38 | 'some-other-episode-file.mp4', 39 | 'http://example.com/some-other-episode-file.mp4'), 40 | ElixirSipsDownloader::Downloadables::File.new( 41 | 'some-other-episode-file.rb', 42 | 'http://example.com/some-other-episode-file.rb'), 43 | ] 44 | ) 45 | } 46 | 47 | let(:feed) { RSS::Parser.parse File.read('spec/fixtures/feed.xml') } 48 | 49 | let(:first_item) { feed.items[0] } 50 | let(:second_item) { feed.items[1] } 51 | 52 | before do 53 | allow(episode_extractor).to receive(:extract).with(first_item) 54 | .and_return(first_episode) 55 | allow(episode_extractor).to receive(:extract).with(second_item) 56 | .and_return(second_episode) 57 | end 58 | 59 | 60 | it 'is an Extractor' do 61 | expect(catalog_extractor).to be_a ElixirSipsDownloader::Extractor 62 | end 63 | 64 | describe '#extract' do 65 | subject(:catalog) { catalog_extractor.extract feed } 66 | 67 | it 'uses Extractors::Episode' do 68 | expect(episode_extractor).to receive(:extract).with(first_item) 69 | .and_return(first_episode) 70 | expect(episode_extractor).to receive(:extract).with(second_item) 71 | .and_return(second_episode) 72 | catalog_extractor.extract feed 73 | end 74 | 75 | it 'returns a Catalog' do 76 | expect(catalog).to eq( 77 | ElixirSipsDownloader::Downloadables::Catalog.new episodes 78 | ) 79 | end 80 | end 81 | end 82 | --------------------------------------------------------------------------------