# frozen_string_literal: true
require 'erb'
require 'uri'
require 'dropcaster/logging'
module Dropcaster
#
# Represents a podcast feed in the RSS 2.0 format
#
class Channel
include Logging
include ERB::Util # for h() in the ERB template
STORAGE_UNITS = %w[Byte KB MB GB TB].freeze
MAX_KEYWORD_COUNT = 12
# Instantiate a new Channel object. +sources+ must be present and can be a String or Array
# of Strings, pointing to a one or more directories or MP3 files.
#
# +attributes+ is a hash with all attributes for the channel. The following attributes are
# mandatory when a new channel is created:
#
# * :title - Title (name) of the podcast
# * :url - URL to the podcast
# * :description - Short description of the podcast (a few words)
#
def initialize(sources, attributes)
# Assert mandatory attributes
%i[title url description].each { |attr|
raise MissingAttributeError.new(attr) if attributes[attr].blank?
}
@attributes = attributes
@attributes[:explicit] = yes_no_or_input(attributes[:explicit])
@source_files = []
# if (sources.respond_to?(:each)) # array
if sources.is_a? Array
sources.each do |src|
add_files(src)
end
else
# single file or directory
add_files(sources)
end
# If not absolute, prepend the image URL with the channel's base to make an absolute URL
unless @attributes[:image_url].blank? || @attributes[:image_url] =~ /^https?:/
logger.info("Channel image URL '#{@attributes[:image_url]}' is relative, so we prepend it with the channel URL '#{@attributes[:url]}'")
@attributes[:image_url] = (URI.parse(@attributes[:url]) + @attributes[:image_url]).to_s
end
# If enclosures_url is not given, take the channel URL as a base.
if @attributes[:enclosures_url].blank?
logger.info("No enclosure URL given, using the channel's enclosure URL")
@attributes[:enclosures_url] = @attributes[:url]
end
# Warn if keyword count is larger than recommended
assert_keyword_count(@attributes[:keywords])
channel_template = @attributes[:channel_template] || File.join(File.dirname(__FILE__), '..', '..', 'templates', 'channel.rss.erb')
begin
@erb_template = ERB.new(IO.read(channel_template))
rescue Errno::ENOENT => e
raise TemplateNotFoundError.new(e.message)
end
end
#
# Returns this channel as an RSS representation. The actual rendering is done with the help
# of an ERB template. By default, it is expected as ../../templates/channel.rss.erb (relative)
# to channel.rb.
#
def to_rss
@erb_template.result(binding)
end
#
# Returns all items (episodes) of this channel, ordered by newest-first.
#
def items
all_items = []
@source_files.each { |src|
item = Item.new(src)
logger.debug("Adding new item from file #{src}")
# set author and image_url from channel if empty
if item.tag.artist.blank?
logger.info("#{src} has no artist, using the channel's author")
item.tag.artist = @attributes[:author]
end
if item.image_url.blank?
logger.info("#{src} has no image URL set, using the channel's image URL")
item.image_url = @attributes[:image_url]
end
# construct absolute URL, based on the channel's enclosures_url attribute
item.url = URI.parse(enclosures_url) + item.file_path.each_filename.map { |p| url_encode(p) }.join('/')
# Warn if keyword count is larger than recommended
assert_keyword_count(item.keywords)
all_items << item
}
all_items.sort { |x, y| y.pub_date <=> x.pub_date }
end
# from http://stackoverflow.com/questions/4136248
def humanize_time(secs)
[[60, :s], [60, :m], [24, :h], [1000, :d]].map { |count, name|
if secs.positive?
secs, n = secs.divmod(count)
"#{n.to_i}#{name}"
end
}.compact.reverse.join(' ')
end
# Fixed version of https://gist.github.com/260184
def humanize_size(number)
return nil if number.nil?
if number.to_i < 1024
unit = number > 1 ? 'Bytes' : 'Byte'
else
max_exp = STORAGE_UNITS.size - 1
number = Float(number)
exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
number /= 1024**exponent
unit = STORAGE_UNITS[exponent]
end
'%n %u'.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
end
#
# Delegate all unknown methods to @attributes
#
# rubocop:disable Style/MethodMissing
def method_missing(meth, *args)
m = meth.id2name
if m =~ /=$/
@attributes[m.chop.to_sym] = (args.length < 2 ? args[0] : args)
else
@attributes[m.to_sym]
end
end
# rubocop:enable Style/MethodMissing
def respond_to_missing?(meth, *)
(meth.id2name =~ /=$/) || super
end
private
def add_files(src)
if File.directory?(src)
@source_files.concat(Dir.glob(File.join(src, '*.mp3'), File::FNM_CASEFOLD))
else
@source_files << src
end
end
#
# Deal with Ruby's autoboxing of Yes, No, true, etc values in YAML
#
def yes_no_or_input(flag)
case flag
when nil
nil
when true
'Yes'
when false
'No'
else
flag
end
end
#
# http://snippets.dzone.com/posts/show/4578
#
def truncate(string, count=30)
if string.length >= count
shortened = string[0, count]
splitted = shortened.split(/\s/)
words = splitted.length
splitted[0, words - 1].join(' ') + '...'
else
string
end
end
def assert_keyword_count(keywords)
if keywords && MAX_KEYWORD_COUNT < keywords.size
logger.info("The list of keywords has #{keywords.size} entries, which exceeds the recommended maximum of #{MAX_KEYWORD_COUNT}.")
end
end
end
end