dropcaster 0.0.6 → 1.2.0

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.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.gemnasium.yml +1 -0
  3. data/.gitignore +12 -14
  4. data/.rubocop.yml +34 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +20 -5
  7. data/CONTRIBUTING.md +18 -0
  8. data/Dockerfile +4 -0
  9. data/Gemfile +2 -0
  10. data/Gemfile.lock +309 -65
  11. data/Guardfile +8 -6
  12. data/LICENSE.txt +1 -1
  13. data/README.markdown +43 -37
  14. data/Rakefile +46 -2
  15. data/{TODO → TODO.markdown} +0 -0
  16. data/VISION.markdown +52 -18
  17. data/Vagrantfile +10 -0
  18. data/bin/dropcaster +57 -53
  19. data/bin/lstags +29 -28
  20. data/dropcaster.gemspec +31 -20
  21. data/lib/dropcaster.rb +2 -15
  22. data/lib/dropcaster/channel.rb +54 -43
  23. data/lib/dropcaster/channel_file_locator.rb +8 -5
  24. data/lib/dropcaster/contributors.rb +39 -0
  25. data/lib/dropcaster/errors.rb +6 -4
  26. data/lib/dropcaster/item.rb +20 -14
  27. data/lib/dropcaster/log_formatter.rb +10 -0
  28. data/lib/dropcaster/logging.rb +13 -0
  29. data/lib/dropcaster/version.rb +3 -1
  30. data/templates/channel.html.erb +3 -6
  31. data/templates/channel.rss.erb +6 -5
  32. data/test/Vagrantfile +3 -1
  33. data/test/bin/vagrant-status +37 -31
  34. data/test/extensions/windows.rb +3 -1
  35. data/test/fixtures/extension.MP3 +0 -0
  36. data/test/fixtures/special &.mp3 +0 -0
  37. data/test/fixtures/test_template.json.erb +3 -4
  38. data/test/helper.rb +4 -1
  39. data/test/unit/test_app.rb +29 -24
  40. data/test/unit/test_channel.rb +9 -5
  41. data/test/unit/test_channel_locator.rb +8 -5
  42. data/test/unit/test_channel_xml.rb +29 -9
  43. data/test/unit/{test_item.rb → test_itunes_item.rb} +5 -6
  44. data/website/.gitignore +2 -0
  45. data/website/README.markdown +8 -0
  46. data/website/_config.yml +14 -0
  47. data/website/_front_matter/contributing.yaml +5 -0
  48. data/website/_front_matter/index.yaml +3 -0
  49. data/website/_front_matter/vision.yaml +5 -0
  50. data/website/_includes/footer.html +55 -0
  51. data/website/_includes/head.html +11 -0
  52. data/website/_includes/header.html +27 -0
  53. data/website/_layouts/default.html +20 -0
  54. data/website/_layouts/page.html +14 -0
  55. data/website/_layouts/post.html +15 -0
  56. data/website/_sass/_base.scss +204 -0
  57. data/website/_sass/_layout.scss +236 -0
  58. data/website/_sass/_syntax-highlighting.scss +67 -0
  59. data/website/css/main.scss +49 -0
  60. data/website/deploy.sh +23 -0
  61. data/website/feed.xml +30 -0
  62. metadata +150 -23
data/bin/lstags CHANGED
@@ -1,51 +1,52 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  unless ARGV.size == 1
4
- STDERR.puts "#{File.basename(__FILE__)}: Missing required parameter for the mp3 file to process"
5
+ warn "#{File.basename(__FILE__)}: Missing required parameter for the mp3 file to process"
5
6
  exit(1)
6
7
  end
7
8
 
8
- require 'rubygems'
9
9
  require 'mp3info'
10
-
11
10
  begin
12
11
  file_name = ARGV.first
13
12
 
14
13
  puts "Listing tags for file: #{file_name}"
15
14
 
15
+ # rubocop:disable Metrics/BlockLength
16
16
  Mp3Info.open(file_name) do |mp3info|
17
17
  puts 'ID3v1 tags:'
18
- mp3info.tag.keys.each{|key|
18
+ mp3info.tag.each_key do |key|
19
19
  puts " #{key} => #{mp3info.tag.send(key)}"
20
- }
20
+ end
21
21
  puts
22
22
  puts 'ID3v2 tags:'
23
- mp3info.tag2.keys.each{|key|
23
+ mp3info.tag2.each_key do |key|
24
24
  case key
25
- when 'PIC'
26
- when 'APIC'
27
- # picture - do not print binary data
28
- when 'ULT'
29
- print " ULT => "
30
- block_counter = 0
31
- mp3info.tag2.ULT.bytes{|b|
32
- print "0x%02x " % b.to_i
33
- print b > 31 ? " '#{b.chr}' " : " " * 5
34
- if (block_counter += 1) > 7 # display in blocks of 8 bytes
35
- puts
36
- print " " * 9
37
- block_counter = 0
38
- end
39
- }
40
- puts
41
- else
42
- puts " #{key} => #{mp3info.tag2.send(key)}"
25
+ when 'PIC'
26
+ when 'APIC'
27
+ # picture - do not print binary data
28
+ when 'ULT'
29
+ print ' ULT => '
30
+ block_counter = 0
31
+ mp3info.tag2.ULT.bytes do |b|
32
+ print format('0x%02x ', b.to_i)
33
+ print b > 31 ? " '#{b.chr}' " : ' ' * 5
34
+ if (block_counter += 1) > 7 # display in blocks of 8 bytes
35
+ puts
36
+ print ' ' * 9
37
+ block_counter = 0
38
+ end
39
+ end
40
+ puts
41
+ else
42
+ puts " #{key} => #{mp3info.tag2.send(key)}"
43
43
  end
44
- }
44
+ end
45
45
  end
46
- puts "Modification date: #{File.new(file_name).mtime}"
46
+ # rubocop:enable Metrics/BlockLength
47
47
 
48
- rescue
49
- puts "Error: #{$!.message}"
48
+ puts "Modification date: #{File.new(file_name).mtime}"
49
+ rescue StandardError
50
+ puts "Error: #{$ERROR_INFO.message}"
50
51
  exit(1)
51
52
  end
data/dropcaster.gemspec CHANGED
@@ -1,37 +1,48 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'dropcaster/version'
5
6
 
7
+ # rubocop:disable Metrics/BlockLength
6
8
  Gem::Specification.new do |spec|
7
- spec.name = "dropcaster"
9
+ spec.name = 'dropcaster'
8
10
  spec.version = Dropcaster::VERSION
9
- spec.authors = ["Nicholas E. Rabenau"]
10
- spec.email = ["nerab@gmx.at"]
11
- spec.summary = %q{Simple Podcast Publishing with Dropbox}
12
- spec.description = %q{Dropcaster is a podcast feed generator for the command line. It is most simple to use with Dropbox, but works equally well with any other hoster.}
13
- spec.homepage = "https://github.com/nerab/dropcaster"
14
- spec.license = "MIT"
11
+ spec.authors = ['Nicholas E. Rabenau']
12
+ spec.email = ['nerab@gmx.at']
13
+ spec.summary = 'Simple Podcast Publishing with Dropbox'
14
+ spec.description = 'Dropcaster is a podcast feed generator for the command line. It is most simple to use with Dropbox, but works equally well with any other hoster.'
15
+ spec.homepage = 'https://github.com/nerab/dropcaster'
16
+ spec.license = 'MIT'
15
17
 
16
18
  spec.files = `git ls-files -z`.split("\x0")
17
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
21
+ spec.require_paths = ['lib']
20
22
 
21
- spec.add_dependency 'ruby-mp3info'
22
23
  spec.add_dependency 'activesupport'
23
24
  spec.add_dependency 'bundler'
25
+ spec.add_dependency 'nokogiri'
26
+ spec.add_dependency 'null-logger'
27
+ spec.add_dependency 'ruby-mp3info'
24
28
 
25
- spec.add_development_dependency 'minitest'
26
- spec.add_development_dependency 'rake'
27
- spec.add_development_dependency 'libxml-ruby'
28
- spec.add_development_dependency 'guard-minitest'
29
+
30
+ spec.add_development_dependency 'bundler', '>= 2.1'
31
+ spec.add_development_dependency 'faraday'
32
+ spec.add_development_dependency 'github-pages'
29
33
  spec.add_development_dependency 'guard-bundler'
34
+ spec.add_development_dependency 'guard-minitest'
30
35
  spec.add_development_dependency 'libnotify'
31
- spec.add_development_dependency 'rb-inotify'
32
- spec.add_development_dependency 'rb-fsevent'
36
+ spec.add_development_dependency 'libxml-ruby'
37
+ spec.add_development_dependency 'minitest'
38
+ spec.add_development_dependency 'octokit'
33
39
  spec.add_development_dependency 'pry'
34
- spec.add_development_dependency 'pry-nav'
35
- spec.add_development_dependency 'pry-stack_explorer'
36
- spec.add_development_dependency 'rb-readline'
40
+ spec.add_development_dependency 'pry-byebug'
41
+ spec.add_development_dependency 'rake'
42
+ spec.add_development_dependency 'rb-fsevent'
43
+ spec.add_development_dependency 'rb-inotify'
44
+ spec.add_development_dependency 'rubocop-minitest'
45
+ spec.add_development_dependency 'rubocop-rake'
46
+ spec.add_development_dependency 'rubocop'
37
47
  end
48
+ # rubocop:enable Metrics/BlockLength
data/lib/dropcaster.rb CHANGED
@@ -1,17 +1,12 @@
1
- $:.unshift File.dirname(__FILE__)
1
+ # frozen_string_literal: true
2
2
 
3
- require 'rubygems'
4
- require 'bundler/setup'
3
+ $LOAD_PATH.unshift File.dirname(__FILE__)
5
4
 
6
5
  require 'yaml'
7
6
  require 'active_support/core_ext/date_time/conversions'
8
7
  require 'active_support/core_ext/object/blank'
9
- require 'active_support/core_ext/module/attribute_accessors'
10
-
11
- require 'logger'
12
8
 
13
9
  require 'dropcaster/errors'
14
- require 'dropcaster/log_formatter'
15
10
  require 'dropcaster/channel'
16
11
  require 'dropcaster/item'
17
12
  require 'dropcaster/channel_file_locator'
@@ -19,12 +14,4 @@ require 'dropcaster/version'
19
14
 
20
15
  module Dropcaster
21
16
  CHANNEL_YML = 'channel.yml'
22
-
23
- mattr_accessor :logger
24
-
25
- unless @@logger
26
- @@logger = Logger.new(STDERR)
27
- @@logger.level = Logger::WARN
28
- @@logger.formatter = LogFormatter.new
29
- end
30
17
  end
@@ -1,14 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erb'
2
4
  require 'uri'
5
+ require 'dropcaster/logging'
3
6
 
4
7
  module Dropcaster
5
8
  #
6
9
  # Represents a podcast feed in the RSS 2.0 format
7
10
  #
8
11
  class Channel
12
+ include Logging
9
13
  include ERB::Util # for h() in the ERB template
10
14
 
11
- STORAGE_UNITS = %w(Byte KB MB GB TB)
15
+ STORAGE_UNITS = %w[Byte KB MB GB TB].freeze
12
16
  MAX_KEYWORD_COUNT = 12
13
17
 
14
18
  # Instantiate a new Channel object. +sources+ must be present and can be a String or Array
@@ -23,13 +27,13 @@ module Dropcaster
23
27
  #
24
28
  def initialize(sources, attributes)
25
29
  # Assert mandatory attributes
26
- [:title, :url, :description].each{|attr|
30
+ %i[title url description].each { |attr|
27
31
  raise MissingAttributeError.new(attr) if attributes[attr].blank?
28
32
  }
29
33
 
30
34
  @attributes = attributes
31
35
  @attributes[:explicit] = yes_no_or_input(attributes[:explicit])
32
- @source_files = Array.new
36
+ @source_files = []
33
37
 
34
38
  # if (sources.respond_to?(:each)) # array
35
39
  if sources.is_a? Array
@@ -43,13 +47,13 @@ module Dropcaster
43
47
 
44
48
  # If not absolute, prepend the image URL with the channel's base to make an absolute URL
45
49
  unless @attributes[:image_url].blank? || @attributes[:image_url] =~ /^https?:/
46
- Dropcaster.logger.info("Channel image URL '#{@attributes[:image_url]}' is relative, so we prepend it with the channel URL '#{@attributes[:url]}'")
50
+ logger.info("Channel image URL '#{@attributes[:image_url]}' is relative, so we prepend it with the channel URL '#{@attributes[:url]}'")
47
51
  @attributes[:image_url] = (URI.parse(@attributes[:url]) + @attributes[:image_url]).to_s
48
52
  end
49
53
 
50
54
  # If enclosures_url is not given, take the channel URL as a base.
51
55
  if @attributes[:enclosures_url].blank?
52
- Dropcaster.logger.info("No enclosure URL given, using the channel's enclosure URL")
56
+ logger.info("No enclosure URL given, using the channel's enclosure URL")
53
57
  @attributes[:enclosures_url] = @attributes[:url]
54
58
  end
55
59
 
@@ -59,7 +63,7 @@ module Dropcaster
59
63
  channel_template = @attributes[:channel_template] || File.join(File.dirname(__FILE__), '..', '..', 'templates', 'channel.rss.erb')
60
64
 
61
65
  begin
62
- @erb_template = ERB.new(IO.read(channel_template), 0, "%<>")
66
+ @erb_template = ERB.new(IO.read(channel_template))
63
67
  rescue Errno::ENOENT => e
64
68
  raise TemplateNotFoundError.new(e.message)
65
69
  end
@@ -78,26 +82,25 @@ module Dropcaster
78
82
  # Returns all items (episodes) of this channel, ordered by newest-first.
79
83
  #
80
84
  def items
81
- all_items = Array.new
82
- @source_files.each{|src|
83
-
85
+ all_items = []
86
+ @source_files.each { |src|
84
87
  item = Item.new(src)
85
88
 
86
- Dropcaster.logger.debug("Adding new item from file #{src}")
89
+ logger.debug("Adding new item from file #{src}")
87
90
 
88
91
  # set author and image_url from channel if empty
89
- if item.artist.blank?
90
- Dropcaster.logger.info("#{src} has no artist, using the channel's author")
92
+ if item.tag.artist.blank?
93
+ logger.info("#{src} has no artist, using the channel's author")
91
94
  item.tag.artist = @attributes[:author]
92
95
  end
93
96
 
94
97
  if item.image_url.blank?
95
- Dropcaster.logger.info("#{src} has no image URL set, using the channel's image URL")
98
+ logger.info("#{src} has no image URL set, using the channel's image URL")
96
99
  item.image_url = @attributes[:image_url]
97
100
  end
98
101
 
99
102
  # construct absolute URL, based on the channel's enclosures_url attribute
100
- item.url = (URI.parse(enclosures_url) + URI.encode(item.file_name))
103
+ item.url = URI.parse(enclosures_url) + item.file_path.each_filename.map { |p| url_encode(p) }.join('/')
101
104
 
102
105
  # Warn if keyword count is larger than recommended
103
106
  assert_keyword_count(item.keywords)
@@ -105,13 +108,13 @@ module Dropcaster
105
108
  all_items << item
106
109
  }
107
110
 
108
- all_items.sort{|x, y| y.pub_date <=> x.pub_date}
111
+ all_items.sort { |x, y| y.pub_date <=> x.pub_date }
109
112
  end
110
113
 
111
114
  # from http://stackoverflow.com/questions/4136248
112
115
  def humanize_time(secs)
113
- [[60, :s], [60, :m], [24, :h], [1000, :d]].map{ |count, name|
114
- if secs > 0
116
+ [[60, :s], [60, :m], [24, :h], [1000, :d]].map { |count, name|
117
+ if secs.positive?
115
118
  secs, n = secs.divmod(count)
116
119
  "#{n.to_i}#{name}"
117
120
  end
@@ -122,37 +125,43 @@ module Dropcaster
122
125
  def humanize_size(number)
123
126
  return nil if number.nil?
124
127
 
125
- storage_units_format = '%n %u'
126
-
127
128
  if number.to_i < 1024
128
129
  unit = number > 1 ? 'Bytes' : 'Byte'
129
- return storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
130
130
  else
131
131
  max_exp = STORAGE_UNITS.size - 1
132
132
  number = Float(number)
133
133
  exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
134
134
  exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
135
- number /= 1024 ** exponent
136
-
135
+ number /= 1024**exponent
137
136
  unit = STORAGE_UNITS[exponent]
138
- return storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
139
137
  end
138
+
139
+ '%n %u'.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
140
140
  end
141
141
 
142
- # delegate all unknown methods to @attributes
142
+ #
143
+ # Delegate all unknown methods to @attributes
144
+ #
145
+ # rubocop:disable Style/MethodMissing
143
146
  def method_missing(meth, *args)
144
147
  m = meth.id2name
145
- if /=$/ =~ m
148
+ if m =~ /=$/
146
149
  @attributes[m.chop.to_sym] = (args.length < 2 ? args[0] : args)
147
150
  else
148
151
  @attributes[m.to_sym]
149
152
  end
150
153
  end
154
+ # rubocop:enable Style/MethodMissing
155
+
156
+ def respond_to_missing?(meth, *)
157
+ (meth.id2name =~ /=$/) || super
158
+ end
159
+
160
+ private
151
161
 
152
- private
153
162
  def add_files(src)
154
163
  if File.directory?(src)
155
- @source_files.concat(Dir.glob(File.join(src, '*.mp3')))
164
+ @source_files.concat(Dir.glob(File.join(src, '*.mp3'), File::FNM_CASEFOLD))
156
165
  else
157
166
  @source_files << src
158
167
  end
@@ -163,12 +172,12 @@ module Dropcaster
163
172
  #
164
173
  def yes_no_or_input(flag)
165
174
  case flag
166
- when nil
167
- nil
168
- when true
169
- 'Yes'
170
- when false
171
- 'No'
175
+ when nil
176
+ nil
177
+ when true
178
+ 'Yes'
179
+ when false
180
+ 'No'
172
181
  else
173
182
  flag
174
183
  end
@@ -177,19 +186,21 @@ module Dropcaster
177
186
  #
178
187
  # http://snippets.dzone.com/posts/show/4578
179
188
  #
180
- def truncate(string, count = 30)
181
- if string.length >= count
182
- shortened = string[0, count]
183
- splitted = shortened.split(/\s/)
184
- words = splitted.length
185
- splitted[0, words - 1].join(' ') + '...'
186
- else
187
- string
188
- end
189
+ def truncate(string, count=30)
190
+ if string.length >= count
191
+ shortened = string[0, count]
192
+ splitted = shortened.split(/\s/)
193
+ words = splitted.length
194
+ splitted[0, words - 1].join(' ') + '...'
195
+ else
196
+ string
197
+ end
189
198
  end
190
199
 
191
200
  def assert_keyword_count(keywords)
192
- Dropcaster.logger.info("The list of keywords has #{keywords.size} entries, which exceeds the recommended maximum of #{MAX_KEYWORD_COUNT}.") if keywords && MAX_KEYWORD_COUNT < keywords.size
201
+ if keywords && MAX_KEYWORD_COUNT < keywords.size
202
+ logger.info("The list of keywords has #{keywords.size} entries, which exceeds the recommended maximum of #{MAX_KEYWORD_COUNT}.")
203
+ end
193
204
  end
194
205
  end
195
206
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dropcaster
2
4
  #
3
5
  # Encapsulates the strategy how to find the channel definition file
@@ -27,9 +29,9 @@ module Dropcaster
27
29
 
28
30
  if sources.respond_to?(:at)
29
31
  # More than one source given. Check that they are all in the same directory.
30
- distinct_dirs = sources.collect{|source| dir_or_self(source)}.uniq
32
+ distinct_dirs = sources.collect { |source| dir_or_self(source) }.uniq
31
33
 
32
- if 1 == distinct_dirs.size
34
+ if distinct_dirs.size == 1
33
35
  # If all are the in same directory, use that as source directory where channel.yml is expected.
34
36
  channel_source_dir = distinct_dirs.first
35
37
  else
@@ -40,11 +42,12 @@ module Dropcaster
40
42
  # If a single file or directory is given, use that as source directory where channel.yml is expected.
41
43
  channel_source_dir = dir_or_self(sources)
42
44
  end
43
-
45
+
44
46
  File.join(channel_source_dir, CHANNEL_YML)
45
47
  end
46
-
47
- private
48
+
49
+ private
50
+
48
51
  def dir_or_self(source)
49
52
  if File.directory?(source)
50
53
  source
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'octokit'
4
+
5
+ module Dropcaster
6
+ class << self
7
+ def contributors
8
+ @octokit ||= if ENV.include?('GH_TOKEN')
9
+ Octokit::Client.new(access_token: ENV['GH_TOKEN'])
10
+ else
11
+ Octokit::Client.new
12
+ end
13
+
14
+ @octokit.contributors('nerab/dropcaster', true).
15
+ sort { |x, y| y.contributions <=> x.contributions }.
16
+ map { |c| "* #{contributor_summary(c)}" }.
17
+ join("\n")
18
+ end
19
+
20
+ def contributor_summary(contributor)
21
+ contributions = contributor.contributions
22
+ "#{contributor_link(contributor)} (#{contributions} contribution#{contributions == 1 ? '' : 's'})"
23
+ end
24
+
25
+ def contributor_link(contributor)
26
+ if contributor.type == 'Anonymous'
27
+ contributor.name.tr('[]', '()')
28
+ else
29
+ # rubocop:disable Style/RescueStandardError
30
+ begin
31
+ "[#{@octokit.user(contributor.login).name}](#{contributor.html_url})"
32
+ rescue
33
+ contributor.login
34
+ end
35
+ # rubocop:enable Style/RescueStandardError
36
+ end
37
+ end
38
+ end
39
+ end