fede 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a752e40bbfd39587eb3274f7218b258fab48beecb7980ac2dbe94e860c068949
4
+ data.tar.gz: 2681a19b6935dd2362b5221d2d64200b2fffcae18308621c8aa3a9cc915fb64e
5
+ SHA512:
6
+ metadata.gz: e823a5bbf61bc740129397248f508c050cd8d2273c763aad3fd2e97631a4cdb6b043703c83915e963feb6d57d6c77e9d021bc7f4c38e128f3ee956dae1777505
7
+ data.tar.gz: 2a4cb095a593c5faa6a404ad17bf8920b1a439b1deecd64ba088b91bf25749721428673ec074f63e443612ef978117abf71382d1ae2d1fbdf405a3d8c8a7bb71
data/bin/fede ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fede'
4
+
5
+ def show_help
6
+ puts 'Usage: fede <config_file> <data_dir> [mode]'
7
+ puts ' modes:'
8
+ puts ' generate: generate feed from scratch'
9
+ puts ' append-available: append the items that have the audio files present'
10
+ puts ' append[-n]: appends the last -n items to the end of the feed, default is 1'
11
+ puts ' example: fede config_file data_dir append-3 (append last 3 items)'
12
+ exit
13
+ end
14
+
15
+ show_help unless ARGV[0]
16
+ show_help unless ARGV[1]
17
+ mode = ARGV[2] || 'generate'
18
+
19
+ case ARGV[0]
20
+ when '-h'
21
+ show_help
22
+ end
23
+
24
+ Fede.run config_file: ARGV[0], data_dir: ARGV[1], mode: mode
@@ -0,0 +1,286 @@
1
+ require 'yaml'
2
+ require 'date'
3
+
4
+ class Fede
5
+ class FeedGenerator
6
+ def initialize(site_config, data_directory)
7
+ @config = parse_yaml site_config
8
+ @feed_file = get_setting 'feed_file'
9
+ @data = parse_data data_directory
10
+ @episode_list = []
11
+ @ep_name = get_setting 'ep_name'
12
+ @ep_pub_date = get_setting 'ep_pub_date'
13
+ @ep_url = get_setting 'ep_url'
14
+ @ep_desc = get_setting 'ep_desc'
15
+ @ep_img = get_setting 'ep_img'
16
+ @ep_details = get_setting 'ep_details'
17
+ @season_name = get_setting 'season_name'
18
+ @season_episode_list = get_setting 'season_episode_list'
19
+ load_episode_list
20
+ end
21
+
22
+ def generate
23
+ output_feed
24
+ puts "#{@feed_file} written!"
25
+ end
26
+
27
+ def append(item_count = 1)
28
+ last_n_episodes = []
29
+ item_count.times.sort_by(&:-@).each do |i|
30
+ last_n_episodes << generate_episode_item(@episode_list[-i - 1])
31
+ end
32
+ do_append last_n_episodes
33
+ end
34
+
35
+ def append_available_files
36
+ episodes = []
37
+ @episode_list.each do |ep|
38
+ next unless File.file? ep['url']
39
+
40
+ episodes << ep
41
+ end
42
+ do_append episodes
43
+ end
44
+
45
+ def do_append(last_n_episodes)
46
+ File.open(@feed_file, 'r+') do |file|
47
+ insert_position = Kernel.loop do
48
+ pos = file.pos
49
+ break pos if file.gets.include?('</channel>') || file.eof?
50
+ end
51
+ file.seek(insert_position, IO::SEEK_SET)
52
+ footer = file.read
53
+ file.seek(insert_position, IO::SEEK_SET)
54
+ episodes_string = last_n_episodes.reduce('') { |prev, ep| "#{prev}#{ep.to_s(2)}" }
55
+ file.write("#{episodes_string}#{footer}")
56
+ end
57
+ puts "Last #{last_n_episodes.length} episode(s) appended to #{@feed_file}!"
58
+ rescue Errno::ENOENT
59
+ puts "Cannot append if feed doesn't exist"
60
+ end
61
+
62
+ private
63
+
64
+ def parse_data(path)
65
+ data = {}
66
+ Dir.entries(path).each do |file|
67
+ next if ['.', '..'].include? file
68
+
69
+ data[file.split('.')[0]] = YAML.load_file "#{path}/#{file}"
70
+ end
71
+ data
72
+ end
73
+
74
+ def parse_yaml(file)
75
+ YAML.load_file file
76
+ end
77
+
78
+ def get_setting(setting_name)
79
+ setting = @config['podcast'][setting_name] || @config[setting_name]
80
+ if setting.nil?
81
+ raise StandardError, "Error: setting #{setting_name} is not defined in the config file, cannot continue"
82
+ end
83
+
84
+ setting
85
+ end
86
+
87
+ def output_feed
88
+ build_date = DateTime.now.strftime(get_setting('datetime_format_string'))
89
+ base_url = get_setting 'url'
90
+ managing_editor = get_setting 'managing_editor'
91
+ editor_email = get_setting 'editor_email'
92
+ desc = get_setting 'description'
93
+
94
+ feed = XMLFeed.new
95
+ channel = XMLNode.new 'channel'
96
+ atom_link = XMLNode.new 'atom:link'
97
+ atom_link.set_propperty 'href', "#{base_url}/#{@feed_file}"
98
+ atom_link.set_propperty 'rel', 'self'
99
+ atom_link.set_propperty 'type', 'application/rss+xml'
100
+ channel.add_child atom_link
101
+ channel.add_child XMLNode.new 'title', content: get_setting('title')
102
+ channel.add_child XMLNode.new 'pubDate', content: @episode_list[0]['pub_date']
103
+ channel.add_child XMLNode.new 'lastBuildDate', content: build_date
104
+ channel.add_child XMLNode.new 'link', content: base_url
105
+ channel.add_child XMLNode.new 'language', content: 'pt'
106
+ channel.add_child(
107
+ XMLNode.new(
108
+ 'copyright',
109
+ cdata: true,
110
+ content: "#{get_setting('title')} #{DateTime.now.year}, #{get_setting('copyright')}."
111
+ )
112
+ )
113
+ channel.add_child XMLNode.new('docs', content: base_url)
114
+ channel.add_child XMLNode.new('managingEditor', content: "#{editor_email} (#{managing_editor})")
115
+ channel.add_child XMLNode.new('itunes:summary', cdata: true, content: desc)
116
+ image = XMLNode.new 'image'
117
+ image.add_child XMLNode.new('url', content: "#{base_url}#{get_setting('logo')}")
118
+ image.add_child XMLNode.new('title', content: get_setting('title'))
119
+ image.add_child XMLNode.new('link', cdata: true, content: base_url)
120
+ channel.add_child image
121
+ channel.add_child XMLNode.new('itunes:author', content: get_setting('author'))
122
+ category = XMLNode.new 'itunes:category'
123
+ category.set_propperty 'text', 'Society &amp; Culture'
124
+ channel.add_child category
125
+ channel.add_child XMLNode.new('itunes:keywords', content: get_setting('keywords'))
126
+ channel.add_child XMLNode.new('itunes:image', propperties: { href: "#{base_url}#{get_setting('logo')}" })
127
+ channel.add_child XMLNode.new('itunes:explicit', content: true)
128
+ owner = XMLNode.new 'itunes:owner'
129
+ owner.add_child XMLNode.new('itunes:email', content: get_setting('email'))
130
+ owner.add_child XMLNode.new('itunes:name', content: get_setting('title'))
131
+ channel.add_child owner
132
+ channel.add_child(XMLNode.new('description', content: desc, cdata: true))
133
+ channel.add_child(
134
+ XMLNode.new(
135
+ 'itunes:subtitle',
136
+ content: get_setting('short_description'),
137
+ cdata: true
138
+ )
139
+ )
140
+ channel.add_child XMLNode.new('itunes:type', content: 'episodic')
141
+ channel.add_child XMLNode.new('itunes:new-feed-url', content: "#{base_url}/#{@feed_file}")
142
+ channel.add_children generate_all_episode_items
143
+
144
+ feed.nodes << channel
145
+ File.open(@feed_file, 'w') { |file| file.write(feed) }
146
+ end
147
+
148
+ def generate_episode_item(episode)
149
+ description = format_description episode[@ep_desc], details: episode[@ep_details], indent_level: 3
150
+ subtitle = format_subtitle episode[@ep_desc]
151
+
152
+ current_item = XMLNode.new 'item'
153
+ current_item.add_child XMLNode.new('guid', content: "#{get_setting('url')}#{episode[@ep_url]}")
154
+ current_item.add_child XMLNode.new('title', content: episode[@ep_name])
155
+ current_item.add_child XMLNode.new('pubDate', content: episode[@ep_pub_date])
156
+ current_item.add_child(XMLNode.new('link', cdata: true, content: "#{get_setting('url')}#{episode[@ep_url]}"))
157
+ current_item.add_child XMLNode.new(
158
+ 'itunes:image',
159
+ propperties: { href: "#{get_setting('url')}#{episode_image(episode)}" }
160
+ )
161
+ current_item.add_child XMLNode.new('description', cdata: true, content: description)
162
+ current_item.add_child(
163
+ XMLNode.new(
164
+ 'enclosure',
165
+ propperties: {
166
+ length: episode_bytes_length(episode),
167
+ type: 'audio/mpeg',
168
+ url: "#{get_setting('url')}#{episode[@ep_url]}"
169
+ }
170
+ )
171
+ )
172
+ current_item.add_child XMLNode.new 'itunes:duration', content: episode_duration(episode)
173
+ current_item.add_child XMLNode.new 'itunes:explicit', content: true
174
+ current_item.add_child XMLNode.new(
175
+ 'itunes:keywords',
176
+ content: get_setting('keywords')
177
+ )
178
+ current_item.add_child XMLNode.new 'itunes:subtitle', content: subtitle, cdata: true
179
+ current_item.add_child XMLNode.new 'itunes:episodeType', content: 'full'
180
+
181
+ current_item
182
+ end
183
+
184
+ def generate_all_episode_items
185
+ episode_list = []
186
+ @episode_list.map do |episode|
187
+ next if episode['hide']
188
+
189
+ episode_list << generate_episode_item(episode)
190
+ rescue StandardError => e
191
+ puts "Failed to generate item for #{episode[@ep_name]}: #{e}"
192
+ end
193
+ episode_list
194
+ end
195
+
196
+ def format_description(description, details: '', indent_level: 0, strip_all: false)
197
+ indentation = "\t" * indent_level
198
+ description = "#{description}\n#{indentation}#{details}".gsub '</br>', "\n#{indentation}"
199
+ description.gsub! '<p>', ''
200
+ description.gsub! '</p>', "\n#{indentation}"
201
+ description.gsub! '<ul>', ''
202
+ description.gsub! '<li>', "\n#{indentation} + "
203
+ description.gsub! '</li>', ''
204
+ description.gsub! '</ul>', "\n#{indentation}"
205
+
206
+ if strip_all
207
+ description.gsub!(/<.?[^>]+>/, '')
208
+ else
209
+ # strip rest of html tags (a tags are allowed)
210
+ description.gsub!(%r{<[^a][^a]/?[^>]+>}, '')
211
+ end
212
+ description.gsub!(" \n", "\n")
213
+ description.strip
214
+ end
215
+
216
+ def format_subtitle(subtitle)
217
+ desc = format_description(subtitle, strip_all: true)
218
+ desc.length > 255 ? "#{desc.slice(0, 252)}..." : desc
219
+ end
220
+
221
+ def episode_bytes_length(episode)
222
+ return episode['bytes_length'] if episode['bytes_length']
223
+
224
+ File.new("#{Dir.getwd}#{episode[@ep_url]}").size
225
+ end
226
+
227
+ def episode_duration(episode)
228
+ return episode['duration'] if episode['duration']
229
+
230
+ raise 'FFMPEG not found. ffmpeg is needed to fech episode length' unless which('ffmpeg')
231
+
232
+ cmd = "ffmpeg -i #{Dir.getwd}#{episode[@ep_url]} 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/\.[0-9]*,//"
233
+ `#{cmd}`.strip!
234
+ end
235
+
236
+ def episode_image(episode)
237
+ episode[@ep_img] || get_setting('logo')
238
+ end
239
+
240
+ def which(cmd)
241
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
242
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
243
+ exts.each do |ext|
244
+ exe = File.join(path, "#{cmd}#{ext}")
245
+ return exe if File.executable?(exe) && !File.directory?(exe)
246
+ end
247
+ end
248
+ nil
249
+ end
250
+
251
+ def load_episode_list
252
+ episodes
253
+ episodes_from_seasons
254
+ @episode_list.sort_by! do |episode|
255
+ DateTime.strptime(
256
+ episode[@ep_pub_date],
257
+ get_setting('datetime_format_string')
258
+ )
259
+ end
260
+ end
261
+
262
+ def episodes
263
+ episode_data = get_setting 'episode_data'
264
+ puts 'No episodes found' && return if episode_data.nil?
265
+
266
+ episode_data.each do |file|
267
+ puts "No data found for #{file}" && next unless @data[file]
268
+
269
+ @episode_list += @data[file]
270
+ end
271
+ end
272
+
273
+ def episodes_from_seasons
274
+ season_data = get_setting 'season_data'
275
+ puts 'No seasons found' && return if season_data.nil?
276
+
277
+ season_data.each do |file|
278
+ puts "No data found for #{file}" && next unless @data[file]
279
+
280
+ @data[file].each do |season|
281
+ @episode_list += season[@season_episode_list]
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,29 @@
1
+ class Fede
2
+ class XMLFeed
3
+ attr_accessor :nodes
4
+
5
+ def initialize(nodes = [])
6
+ @nodes = nodes
7
+ end
8
+
9
+ def header
10
+ "<?xml version='1.0' encoding='UTF-8'?>\n<rss version='2.0' xmlns:atom='http://www.w3.org/2005/Atom'"\
11
+ " xmlns:cc='http://web.resource.org/cc/' xmlns:itunes='http://www.itunes.com/dtds/podcast-1.0.dtd'"\
12
+ " xmlns:media='http://search.yahoo.com/mrss/' xmlns:content='http://purl.org/rss/1.0/modules/content/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n"
13
+ end
14
+
15
+ def footer
16
+ "</rss>\n"
17
+ end
18
+
19
+ def to_s
20
+ '' unless @nodes
21
+
22
+ nodes_string = @nodes.reduce('') do |prev, node|
23
+ "#{prev}#{node.to_s(1)}"
24
+ end
25
+
26
+ "#{header}#{nodes_string}#{footer}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ class Fede
2
+ class XMLNode
3
+ attr_reader :children, :propperties
4
+
5
+ def initialize(tag_name, content: nil, cdata: false, propperties: {})
6
+ @tag_name = tag_name
7
+ @content = content
8
+ @cdata = cdata
9
+ @propperties = propperties
10
+ @children = []
11
+ end
12
+
13
+ def parent?
14
+ !@children.empty?
15
+ end
16
+
17
+ def set_propperty(name, value)
18
+ @propperties[name] = value
19
+ end
20
+
21
+ def add_child(child)
22
+ @children << child
23
+ end
24
+
25
+ def add_children(children)
26
+ raise TypeError 'Children must be an Array of nodes' unless children.is_a? Array
27
+
28
+ @children += children
29
+ end
30
+
31
+ def open_tag(indent_level)
32
+ if @cdata
33
+ "<#{@tag_name}>\n#{"\t" * indent_level}<![CDATA["
34
+ elsif @propperties.empty?
35
+ "<#{@tag_name}>"
36
+ else
37
+ prop_string = @propperties.reduce('') { |prev, values| "#{prev} #{values[0]}='#{values[1]}'" }
38
+ "<#{@tag_name} #{prop_string.strip}#{@content ? '' : ' /'}>"
39
+ end
40
+ end
41
+
42
+ def prop_string
43
+ return '' if @propperties.empty?
44
+
45
+ @propperties.reduce('') { |prev, values| "#{prev} #{values[0]}='#{values[1]}'" }
46
+ end
47
+
48
+ def children_string(indent_level)
49
+ @children.reduce('') { |prev, value| "#{prev}#{value.to_s indent_level}" }
50
+ end
51
+
52
+ def tag_open
53
+ "<#{@tag_name}#{prop_string}"
54
+ end
55
+
56
+ def tag_middle(indent_level)
57
+ return '' unless parent? || @content
58
+
59
+ if @cdata
60
+ ">\n#{"\t" * (indent_level + 1)}<![CDATA[#{@content}]]>\n#{"\t" * indent_level}"
61
+ elsif parent?
62
+ ">\n#{children_string(indent_level + 1)}#{"\t" * indent_level}"
63
+ else
64
+ ">#{@content}"
65
+ end
66
+ end
67
+
68
+ def tag_close
69
+ parent? || @content ? "</#{@tag_name}>\n" : " />\n"
70
+ end
71
+
72
+ def to_s(indent_level = 0)
73
+ "#{"\t" * indent_level}#{tag_open}#{tag_middle(indent_level)}#{tag_close}"
74
+ end
75
+ end
76
+ end
data/lib/fede.rb ADDED
@@ -0,0 +1,33 @@
1
+ class Fede
2
+ def self.run(config_file:, data_dir:, mode:)
3
+ @generator = Fede::FeedGenerator.new config_file, data_dir
4
+ if mode.include? 'append'
5
+ append mode
6
+ elsif mode == 'generate'
7
+ generate
8
+ elsif mode == 'append-available'
9
+ append_available
10
+ else
11
+ puts "Invalid mode #{mode}. Valid modes are 'generate' or 'append'"
12
+ end
13
+ rescue StandardError => e
14
+ puts e
15
+ end
16
+
17
+ def self.append_available
18
+ @generator.append_available_files
19
+ end
20
+
21
+ def self.generate
22
+ @generator.generate
23
+ end
24
+
25
+ def self.append(mode)
26
+ mode_info = mode.split('-')
27
+ @generator.append(mode_info.length > 1 ? Integer(mode_info[1]) : 1)
28
+ end
29
+ end
30
+
31
+ require 'fede/generator'
32
+ require 'fede/xml_feed'
33
+ require 'fede/xml_node'
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fede
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Lucca Augusto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-04-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Very Simple XML feed generator from yaml data files
14
+ email: lucca@luccaaugusto.xyz
15
+ executables:
16
+ - fede
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/fede
21
+ - lib/fede.rb
22
+ - lib/fede/generator.rb
23
+ - lib/fede/xml_feed.rb
24
+ - lib/fede/xml_node.rb
25
+ homepage: https://github.com/luccaugusto/fede
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '3.0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.3.26
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: XML from yaml
48
+ test_files: []