das_catalog 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Brendon Murphy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ Das Catalog
2
+ ===========
3
+
4
+ The Das Catalog gem - used for downloading screencasts from the [Destroy All Software screencasts catalog](https://www.destroyallsoftware.com/screencasts). Uses the rss feed plus Mechanize to log you in and then download unsaved movies to a local directory.
5
+
6
+ This gem is in not linked to Destroy All Software or endorsed by Gary Bernhardt. I just like his stuff.
7
+
8
+ Requirements
9
+ ------------
10
+
11
+ I've written and only run the gem on Ruby 1.9.2. YMMV, but 1.8.x will definitely blow up.
12
+
13
+ The gem also expects that wget exists in your path, as it uses it for downloaded the movie file.
14
+
15
+ Installation
16
+ ------------
17
+
18
+ Install the gem
19
+
20
+ $ gem install das_catalog
21
+
22
+ Usage
23
+ -----
24
+
25
+ Run from the command line:
26
+
27
+ $ das_catalog_sync
28
+
29
+ It will prompt your for some settings (username, password, storage directory) and then be off and running.
30
+
31
+ Stored Configuration
32
+ --------------------
33
+
34
+ If you don't provide configuration, `das_catalog_sync` will prompt you for configuration values when you run it.
35
+
36
+ However, you can persist a full or partial config in ~/.das_catalog.yml as such:
37
+
38
+ ---
39
+ username: your_username
40
+ password: your_password
41
+ downloads_directory: /Users/programmer/Movies/DAS
42
+
43
+ If you wish to exclude any config, leave it out and `das_catalog_sync` will prompt your for it at runtime.
44
+
45
+ Basic Process
46
+ -------------
47
+
48
+ The steps by which the gem works are fairly simple:
49
+
50
+ 1. Fetch the rss feed to see what is in the catalog
51
+ 2. Use Mechanize to log in at the DAS sign in page
52
+ 3. Find links in the feed that have not been downloaded yet
53
+ 4. Construct a download link, and grab the location header from the redirect (the movie on S3)
54
+ 5. Offload the download to wget for efficient processing
55
+
56
+ TODO
57
+ ----
58
+ * Finish missing spec coverage (mostly in DasCatalog toplevel namespace)
59
+ * Hook up Aruba for bin testing
60
+ * Switch bin to [GLI](https://github.com/davetron5000/gli)?
61
+ * CLI interface for tracking if you've watched a specific screencast
62
+ * Make missing directory automatically
63
+ * better wget detection/warnings
64
+ * Lighten some dependencies (feed fetching, highline, etc)
65
+ * Better cli access to modify the underlying pstore
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'highline/import'
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../lib", File.dirname(__FILE__))
7
+
8
+ require 'das_catalog'
9
+
10
+ config_file = File.expand_path("~/.das_catalog.yml")
11
+
12
+ config = if File.exists?(config_file)
13
+ YAML.load_file(config_file)
14
+ else
15
+ {}
16
+ end
17
+
18
+ DasCatalog.configure do |c|
19
+ c.username = config["username"] || ask("Enter your DAS username: ") { |q| q.echo = true }
20
+ c.password = config["password"] || ask("Enter your DAS password: ") { |q| q.echo = "*" }
21
+ c.downloads_directory = config["downloads_directory"] || ask("Enter your DAS downloads directory: ") { |q| q.echo = "*" }
22
+ end
23
+
24
+ DasCatalog.sync
@@ -0,0 +1,18 @@
1
+ require 'shellwords'
2
+
3
+ module DasCatalog
4
+ # TODO spec this
5
+ class Downloader
6
+ def self.process(screencast)
7
+ # Mechanize was choking on large file downloads and keeps them in memory,
8
+ # outsource to wget
9
+ agent = DasCatalog.agent
10
+ agent.redirect_ok = false
11
+ result = agent.get screencast.download_link
12
+ url = result.header["location"]
13
+ url =~ /^http.+?([^\/]+\.mov)\?/
14
+ output_filename = $1
15
+ `wget --quiet #{Shellwords.escape url} -O #{DasCatalog.downloads_directory}/#{output_filename}`
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ require 'feedzirra'
2
+
3
+ module DasCatalog
4
+ class Feed
5
+ FEED_URL = "https://www.destroyallsoftware.com/screencasts/feed"
6
+
7
+ def self.get
8
+ Feedzirra::Feed.fetch_and_parse(FEED_URL)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module DasCatalog
2
+ class Screencast
3
+ extend Forwardable
4
+ include LoggerAccess
5
+
6
+ attr_reader :screencast_data
7
+ def_delegators :@screencast_data, :watched?, :downloaded?, :download_link
8
+
9
+ def initialize(screencast_data)
10
+ @screencast_data = screencast_data
11
+ end
12
+
13
+ def self.for_link(link)
14
+ new ScreencastData.for_link(link)
15
+ end
16
+
17
+ def to_s
18
+ screencast_data.link
19
+ end
20
+
21
+ def download
22
+ return false if screencast_data.downloaded?
23
+ log.info "Starting download of #{self}"
24
+ Downloader.process(self)
25
+ screencast_data.downloaded
26
+ log.info "Finished download of #{self}"
27
+ screencast_data.save
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ module DasCatalog
2
+ class ScreencastData
3
+ attr_reader :link, :watched, :downloaded
4
+
5
+ def initialize(link)
6
+ @link = link
7
+ end
8
+
9
+ def self.for_link(link)
10
+ Store.find(link) or new(link)
11
+ end
12
+
13
+ alias :id :link
14
+
15
+ def download_link
16
+ "#{link}/download"
17
+ end
18
+
19
+ def downloaded
20
+ @downloaded = true
21
+ end
22
+
23
+ def downloaded?
24
+ @downloaded
25
+ end
26
+
27
+ def watched
28
+ @watched = true
29
+ end
30
+
31
+ def unwatched
32
+ @watched = false
33
+ end
34
+
35
+ def watched?
36
+ @watched
37
+ end
38
+
39
+ def save
40
+ Store.store(self)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ require 'logger'
2
+ require 'delegate'
3
+
4
+ module DasCatalog
5
+ class StdLogger < DelegateClass(::Logger)
6
+ def initialize
7
+ @logger = ::Logger.new(STDOUT)
8
+ super(@logger)
9
+ end
10
+ end
11
+
12
+ module LoggerAccess
13
+ def self.included(klass)
14
+ klass.extend(self)
15
+ end
16
+
17
+ def log
18
+ DasCatalog.logger
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ require 'pstore'
2
+
3
+ module DasCatalog
4
+ class Store
5
+ def self.find(id)
6
+ store = PStore.new(DasCatalog.tracker_file)
7
+ store.transaction(true) do
8
+ store[id]
9
+ end
10
+ end
11
+
12
+ def self.store(screencast_data)
13
+ store = PStore.new(DasCatalog.tracker_file)
14
+ store.transaction do
15
+ store[screencast_data.id] = screencast_data
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module DasCatalog
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,55 @@
1
+ require 'mechanize'
2
+
3
+ module DasCatalog
4
+ SIGN_IN_URL = "https://www.destroyallsoftware.com/screencasts/users/sign_in"
5
+
6
+ # TODO spec this
7
+ class << self
8
+ attr_accessor :username
9
+ attr_accessor :password
10
+ attr_accessor :downloads_directory
11
+
12
+ attr_writer :logger
13
+ def logger
14
+ @logger ||= StdLogger.new
15
+ end
16
+
17
+ attr_writer :tracker_file
18
+ def tracker_file
19
+ @tracker_file ||= File.expand_path("~/.das_tracker.pstore")
20
+ end
21
+ end
22
+
23
+ def self.configure
24
+ yield self
25
+ end
26
+
27
+ # TODO spec this
28
+ def self.agent
29
+ @agent ||= begin
30
+ agent = Mechanize.new
31
+ page = agent.get SIGN_IN_URL
32
+ form = page.forms.first
33
+ form['user[email]'] = username
34
+ form['user[password]'] = password
35
+ agent.submit form
36
+ agent
37
+ end
38
+ end
39
+
40
+ # TODO spec this
41
+ def self.sync
42
+ feed = Feed.get
43
+ feed.entries.each do |entry|
44
+ sc = Screencast.for_link(entry.url)
45
+ sc.download
46
+ end
47
+ end
48
+ end
49
+
50
+ require_relative "das_catalog/downloader"
51
+ require_relative "das_catalog/feed"
52
+ require_relative "das_catalog/std_logger"
53
+ require_relative "das_catalog/screencast"
54
+ require_relative "das_catalog/screencast_data"
55
+ require_relative "das_catalog/store"
@@ -0,0 +1,7 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog do
4
+ it "has a DasCatalog::StdLogger by default" do
5
+ assert DasCatalog.logger
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog::Downloader do
4
+ # TODO
5
+ end
6
+
@@ -0,0 +1,8 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog::Feed do
4
+ it "grabs the feed with feedzirra" do
5
+ Feedzirra::Feed.expects(:fetch_and_parse).with("https://www.destroyallsoftware.com/screencasts/feed")
6
+ DasCatalog::Feed.get
7
+ end
8
+ end
@@ -0,0 +1,52 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog::ScreencastData do
4
+ let(:link) { "https://www.destroyallsoftware.com/screencasts/catalog/statistics-over-git-repositories" }
5
+ subject { DasCatalog::ScreencastData.new(link) }
6
+
7
+ it "creates a download link by appending download to the link" do
8
+ subject.download_link.must_equal("#{link}/download")
9
+ end
10
+
11
+ it "uses the link as the id" do
12
+ subject.id.must_equal link
13
+ end
14
+
15
+ it "doesn't report as downloaded if it hasn't been" do
16
+ refute subject.downloaded?
17
+ end
18
+
19
+ it "reports as downloaded if downloaded" do
20
+ subject.downloaded
21
+ assert subject.downloaded?
22
+ end
23
+
24
+ it "reports as watched if watched" do
25
+ subject.watched
26
+ assert subject.watched?
27
+ end
28
+
29
+ it "can be unwatched" do
30
+ subject.unwatched
31
+ refute subject.watched?
32
+ end
33
+
34
+ it "saves by passing itself to the tracker" do
35
+ DasCatalog::Store.expects(:store).with(subject)
36
+ subject.save
37
+ end
38
+
39
+ describe "getting the screencast data for a given link" do
40
+ it "creates a new screencast data if one doeesn't already exist" do
41
+ skip "needs new_record?"
42
+ scd = DasCatalog::ScreencastData.for_link(link)
43
+ assert scd.new_record?
44
+ end
45
+
46
+ it "finds the screencast data if already persisted" do
47
+ DasCatalog::Store.expects(:find).with(link).returns(subject)
48
+ DasCatalog::ScreencastData.for_link(link).must_equal(subject)
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,77 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog::Screencast do
4
+ let(:link) { "https://www.destroyallsoftware.com/screencasts/catalog/statistics-over-git-repositories" }
5
+ let(:screencast_data) { sd = mock("ScreencastData"); sd.stubs(:link).returns(link); sd }
6
+ subject { DasCatalog::Screencast.new(screencast_data) }
7
+
8
+ it "is created from screencast data" do
9
+ screencast = DasCatalog::Screencast.new(screencast_data)
10
+ screencast.screencast_data.must_equal(screencast_data)
11
+ end
12
+
13
+ it "contains screencast data" do
14
+ assert subject.screencast_data
15
+ end
16
+
17
+ it "is converting to a string that is the link" do
18
+ subject.to_s.must_equal link
19
+ end
20
+
21
+ it "can be created for a given link" do
22
+ screencast_data = mock()
23
+ DasCatalog::ScreencastData.expects(:for_link).with(link).returns(screencast_data)
24
+ DasCatalog::Screencast.for_link(link).screencast_data.must_equal screencast_data
25
+ end
26
+
27
+ it "delegates to the screencast data for watched?" do
28
+ screencast_data.stubs(:watched?).returns(false, true)
29
+ refute subject.watched?
30
+ assert subject.watched?
31
+ end
32
+
33
+ it "delegates to the screencast data for downloaded?" do
34
+ screencast_data.stubs(:downloaded?).returns(false, true)
35
+ refute subject.downloaded?
36
+ assert subject.downloaded?
37
+ end
38
+
39
+ it "delegates to the screencast data for download link" do
40
+ screencast_data.stubs(:download_link).returns("http://example.com/path")
41
+ subject.download_link.must_equal "http://example.com/path"
42
+ end
43
+
44
+ describe "downloading" do
45
+ before do
46
+ DasCatalog::Downloader.stubs(:process)
47
+ screencast_data.stubs(:save)
48
+ screencast_data.stubs(:downloaded?)
49
+ screencast_data.stubs(:downloaded)
50
+ end
51
+
52
+ context "for a screencast that hasn't been downloaded" do
53
+ it "occurs by sending itself to the downloader for processing" do
54
+ DasCatalog::Downloader.expects(:process).with(subject)
55
+ subject.download
56
+ end
57
+
58
+ it "marks the screencast data downloaded" do
59
+ screencast_data.expects(:downloaded)
60
+ subject.download
61
+ end
62
+
63
+ it "saves the screencast data" do
64
+ screencast_data.expects(:save)
65
+ subject.download
66
+ end
67
+ end
68
+
69
+ context "for a screencast that hasn't been downloaded" do
70
+ it "returns false" do
71
+ screencast_data.expects(:downloaded?).returns(true)
72
+ refute subject.download
73
+ end
74
+ end
75
+ end
76
+ end
77
+
@@ -0,0 +1,35 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog::StdLogger do
4
+ it "creates a standard ruby logger to STDOUT" do
5
+ ::Logger.expects(:new).with(STDOUT)
6
+ DasCatalog::StdLogger.new
7
+ end
8
+
9
+ [:fatal, :error, :warn, :info, :debug].each do |log_method|
10
+ it "delegates #{log_method} to the contained logger" do
11
+ logger = DasCatalog::StdLogger.new
12
+ assert_respond_to(logger, log_method)
13
+
14
+ end
15
+ end
16
+
17
+ describe "the logger mixin" do
18
+ let(:klass) { Class.new }
19
+
20
+ before do
21
+ DasCatalog.logger = mock("Logger")
22
+ klass.send(:include, DasCatalog::LoggerAccess)
23
+ end
24
+
25
+ it "adds a log class method to grab the DasCatalog.logger" do
26
+ assert klass.respond_to?(:log)
27
+ klass.log.must_equal(DasCatalog.logger)
28
+ end
29
+
30
+ it "also adds a log instance method" do
31
+ assert klass.new.respond_to?(:log)
32
+ klass.log.must_equal(DasCatalog.logger)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe DasCatalog::Store do
4
+ let(:link) { "https://www.destroyallsoftware.com/screencasts/catalog/statistics-over-git-repositories" }
5
+ let(:id) { link }
6
+ let(:screencast_data) { scd = mock(); scd.stubs(:id).returns(id); scd }
7
+
8
+ it "tracks in the file specified in config" do
9
+ DasCatalog::Store.store(screencast_data)
10
+ assert File.exists?(DasCatalog.tracker_file.path)
11
+ end
12
+
13
+ it "can find the screencast by the id" do
14
+ DasCatalog::Store.store(screencast_data)
15
+ assert DasCatalog::Store.find(id)
16
+ end
17
+
18
+ it "is nil if the screencast is not found" do
19
+ refute DasCatalog::Store.find(id)
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ gem "minitest"
2
+ require 'minitest/spec'
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+ require 'mocha'
6
+ require 'fakefs/safe'
7
+ require 'fileutils'
8
+ require 'tempfile'
9
+ require 'logger'
10
+
11
+ require_relative '../lib/das_catalog'
12
+
13
+ alias :context :describe
14
+
15
+ null_logger = ::Logger.new("/dev/null")
16
+
17
+ MiniTest::Spec.add_setup_hook do
18
+ DasCatalog.configure do |c|
19
+ c.tracker_file = Tempfile.new("das_tracker")
20
+ c.logger = null_logger
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: das_catalog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brendon Murphy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-17 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: feedzirra
16
+ requirement: &2153475280 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2153475280
25
+ - !ruby/object:Gem::Dependency
26
+ name: mechanize
27
+ requirement: &2153473140 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2153473140
36
+ - !ruby/object:Gem::Dependency
37
+ name: highline
38
+ requirement: &2153472680 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2153472680
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: &2153472260 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2153472260
58
+ - !ruby/object:Gem::Dependency
59
+ name: mocha
60
+ requirement: &2153471700 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2153471700
69
+ - !ruby/object:Gem::Dependency
70
+ name: fakefs
71
+ requirement: &2153471140 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2153471140
80
+ description: Download screencasts from Destroy All Software catalog. Uses the rss
81
+ feed plus Mechanize to log you in and then download new movies to your local drive.
82
+ email: xternal1+github@gmail.com
83
+ executables:
84
+ - das_catalog_sync
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - lib/das_catalog/downloader.rb
89
+ - lib/das_catalog/feed.rb
90
+ - lib/das_catalog/screencast.rb
91
+ - lib/das_catalog/screencast_data.rb
92
+ - lib/das_catalog/std_logger.rb
93
+ - lib/das_catalog/store.rb
94
+ - lib/das_catalog/version.rb
95
+ - lib/das_catalog.rb
96
+ - bin/das_catalog_sync
97
+ - README.md
98
+ - LICENSE
99
+ - spec/das_catalog_spec.rb
100
+ - spec/das_downloader_spec.rb
101
+ - spec/das_feed_spec.rb
102
+ - spec/das_screencast_data_spec.rb
103
+ - spec/das_screencast_spec.rb
104
+ - spec/das_std_logger_spec.rb
105
+ - spec/das_store_spec.rb
106
+ - spec/spec_helper.rb
107
+ homepage:
108
+ licenses: []
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 1.8.11
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: Download screencasts from Destroy All Software catalog
131
+ test_files:
132
+ - spec/das_catalog_spec.rb
133
+ - spec/das_downloader_spec.rb
134
+ - spec/das_feed_spec.rb
135
+ - spec/das_screencast_data_spec.rb
136
+ - spec/das_screencast_spec.rb
137
+ - spec/das_std_logger_spec.rb
138
+ - spec/das_store_spec.rb
139
+ - spec/spec_helper.rb