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.
- checksums.yaml +5 -5
- data/.gemnasium.yml +1 -0
- data/.gitignore +12 -14
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -5
- data/CONTRIBUTING.md +18 -0
- data/Dockerfile +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +309 -65
- data/Guardfile +8 -6
- data/LICENSE.txt +1 -1
- data/README.markdown +43 -37
- data/Rakefile +46 -2
- data/{TODO → TODO.markdown} +0 -0
- data/VISION.markdown +52 -18
- data/Vagrantfile +10 -0
- data/bin/dropcaster +57 -53
- data/bin/lstags +29 -28
- data/dropcaster.gemspec +31 -20
- data/lib/dropcaster.rb +2 -15
- data/lib/dropcaster/channel.rb +54 -43
- data/lib/dropcaster/channel_file_locator.rb +8 -5
- data/lib/dropcaster/contributors.rb +39 -0
- data/lib/dropcaster/errors.rb +6 -4
- data/lib/dropcaster/item.rb +20 -14
- data/lib/dropcaster/log_formatter.rb +10 -0
- data/lib/dropcaster/logging.rb +13 -0
- data/lib/dropcaster/version.rb +3 -1
- data/templates/channel.html.erb +3 -6
- data/templates/channel.rss.erb +6 -5
- data/test/Vagrantfile +3 -1
- data/test/bin/vagrant-status +37 -31
- data/test/extensions/windows.rb +3 -1
- data/test/fixtures/extension.MP3 +0 -0
- data/test/fixtures/special &.mp3 +0 -0
- data/test/fixtures/test_template.json.erb +3 -4
- data/test/helper.rb +4 -1
- data/test/unit/test_app.rb +29 -24
- data/test/unit/test_channel.rb +9 -5
- data/test/unit/test_channel_locator.rb +8 -5
- data/test/unit/test_channel_xml.rb +29 -9
- data/test/unit/{test_item.rb → test_itunes_item.rb} +5 -6
- data/website/.gitignore +2 -0
- data/website/README.markdown +8 -0
- data/website/_config.yml +14 -0
- data/website/_front_matter/contributing.yaml +5 -0
- data/website/_front_matter/index.yaml +3 -0
- data/website/_front_matter/vision.yaml +5 -0
- data/website/_includes/footer.html +55 -0
- data/website/_includes/head.html +11 -0
- data/website/_includes/header.html +27 -0
- data/website/_layouts/default.html +20 -0
- data/website/_layouts/page.html +14 -0
- data/website/_layouts/post.html +15 -0
- data/website/_sass/_base.scss +204 -0
- data/website/_sass/_layout.scss +236 -0
- data/website/_sass/_syntax-highlighting.scss +67 -0
- data/website/css/main.scss +49 -0
- data/website/deploy.sh +23 -0
- data/website/feed.xml +30 -0
- 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
|
-
|
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.
|
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.
|
23
|
+
mp3info.tag2.each_key do |key|
|
24
24
|
case key
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
46
|
+
# rubocop:enable Metrics/BlockLength
|
47
47
|
|
48
|
-
|
49
|
-
|
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
|
-
#
|
2
|
-
|
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 =
|
9
|
+
spec.name = 'dropcaster'
|
8
10
|
spec.version = Dropcaster::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
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 = [
|
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
|
-
|
26
|
-
spec.add_development_dependency '
|
27
|
-
spec.add_development_dependency '
|
28
|
-
spec.add_development_dependency '
|
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 '
|
32
|
-
spec.add_development_dependency '
|
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-
|
35
|
-
spec.add_development_dependency '
|
36
|
-
spec.add_development_dependency 'rb-
|
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
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
data/lib/dropcaster/channel.rb
CHANGED
@@ -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
|
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
|
-
[
|
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 =
|
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
|
-
|
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
|
-
|
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)
|
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 =
|
82
|
-
@source_files.each{|src|
|
83
|
-
|
85
|
+
all_items = []
|
86
|
+
@source_files.each { |src|
|
84
87
|
item = Item.new(src)
|
85
88
|
|
86
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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
|
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
|
-
#
|
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
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|