podrb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/exe/podrb +5 -0
- data/lib/podrb/cli.rb +117 -0
- data/lib/podrb/commands/add/output.rb +31 -0
- data/lib/podrb/commands/add/runner.rb +68 -0
- data/lib/podrb/commands/archive/output.rb +22 -0
- data/lib/podrb/commands/archive/runner.rb +26 -0
- data/lib/podrb/commands/base_output.rb +23 -0
- data/lib/podrb/commands/base_runner.rb +46 -0
- data/lib/podrb/commands/dearchive/output.rb +22 -0
- data/lib/podrb/commands/dearchive/runner.rb +26 -0
- data/lib/podrb/commands/delete/output.rb +22 -0
- data/lib/podrb/commands/delete/runner.rb +31 -0
- data/lib/podrb/commands/episodes/output.rb +30 -0
- data/lib/podrb/commands/episodes/runner.rb +45 -0
- data/lib/podrb/commands/init/output.rb +30 -0
- data/lib/podrb/commands/init/runner.rb +47 -0
- data/lib/podrb/commands/open/output.rb +18 -0
- data/lib/podrb/commands/open/runner.rb +24 -0
- data/lib/podrb/commands/podcasts/output.rb +30 -0
- data/lib/podrb/commands/podcasts/runner.rb +32 -0
- data/lib/podrb/commands/sync/output.rb +22 -0
- data/lib/podrb/commands/sync/runner.rb +34 -0
- data/lib/podrb/commands/update/output.rb +26 -0
- data/lib/podrb/commands/update/runner.rb +27 -0
- data/lib/podrb/commands.rb +28 -0
- data/lib/podrb/infrastructure/dto.rb +17 -0
- data/lib/podrb/infrastructure/feed_parser.rb +41 -0
- data/lib/podrb/infrastructure/shell_interface.rb +11 -0
- data/lib/podrb/infrastructure/storage/sql.rb +59 -0
- data/lib/podrb/version.rb +5 -0
- data/lib/podrb.rb +8 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1f36c1361363872269b583988f8f458d5ddf6fb69c937c9e2833e9a33273d6a5
|
4
|
+
data.tar.gz: f7134ddd6f79bd836b1c62dfdf903a2dc69821831b8e8aab14f0f405dc0205cf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9a64836ce14be0068c5dad6317745caf62668350008dbc84d8b7a67d66375778e11504bdf79cae647661bcaaf775e4a24b0ff2c18c35133333ffd06af9d5b085
|
7
|
+
data.tar.gz: 15640e0610b45b9177fdcd471050d549b093282fea329d31740201e4ad627edf94782ac18ac234aabe00dd1e8ee29aa7867ff944c4e8f39461f42d9c61a0cb8f
|
data/exe/podrb
ADDED
data/lib/podrb/cli.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
require_relative "commands"
|
6
|
+
require_relative "infrastructure/storage/sql"
|
7
|
+
require_relative "infrastructure/shell_interface"
|
8
|
+
|
9
|
+
module Podrb
|
10
|
+
class CLI < Thor
|
11
|
+
# https://github.com/rails/thor/issues/728#issuecomment-642798887
|
12
|
+
def self.exit_on_failure?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.start(given_args = ARGV, config = {})
|
17
|
+
command = given_args.first
|
18
|
+
does_not_require_config = %w[version -V --version init].include?(command)
|
19
|
+
podrb_initialized = Dir.exist?(ENV["HOME"] + "/.config/podrb")
|
20
|
+
if does_not_require_config || podrb_initialized
|
21
|
+
super
|
22
|
+
else
|
23
|
+
puts "Missing config files. Run `podrb init` first."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "version", "Displays the podrb version"
|
28
|
+
map %w[-V --version] => :version
|
29
|
+
def version
|
30
|
+
puts VERSION
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "init", "Creates the podrb config files"
|
34
|
+
def init
|
35
|
+
puts "Creating config files..."
|
36
|
+
|
37
|
+
result = Podrb::Commands::Init::Runner.call
|
38
|
+
|
39
|
+
puts Podrb::Commands::Init::Output.call(result)
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "add FEED", "Adds a podcast to the Podrb database"
|
43
|
+
method_option :sync_url, type: :string, default: "", desc: "Podrb will use this URL to sync the podcast."
|
44
|
+
def add(feed)
|
45
|
+
puts "Adding the podcast..."
|
46
|
+
|
47
|
+
result = Podrb::Commands::Add::Runner.call(feed, options)
|
48
|
+
|
49
|
+
puts Podrb::Commands::Add::Output.call(result)
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "podcasts", "List the podcast records"
|
53
|
+
method_option :fields, type: :array, default: [], desc: "Select the fields that will be displayed."
|
54
|
+
def podcasts
|
55
|
+
result = Podrb::Commands::Podcasts::Runner.call(options)
|
56
|
+
|
57
|
+
puts Podrb::Commands::Podcasts::Output.call(result)
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "episodes PODCAST_ID", "List the podcast episodes"
|
61
|
+
method_option :fields, type: :array, default: [], desc: "Select the fields that will be displayed."
|
62
|
+
method_option :order_by, type: :string, default: "id", desc: "Choose how podrb will order the episodes."
|
63
|
+
method_option :all, type: :boolean, default: false, desc: "List archived episodes too."
|
64
|
+
def episodes(podcast_id)
|
65
|
+
result = Podrb::Commands::Episodes::Runner.call(podcast_id, options)
|
66
|
+
|
67
|
+
puts Podrb::Commands::Episodes::Output.call(result)
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "open EPISODE_ID", "Open a episode in the browser"
|
71
|
+
method_option :browser, type: :string, default: "firefox", desc: "Choose the browser."
|
72
|
+
method_option :archive, type: :boolean, default: false, desc: "Archive the episode."
|
73
|
+
def open(episode_id)
|
74
|
+
result = Podrb::Commands::Open::Runner.call(episode_id, options)
|
75
|
+
|
76
|
+
Infrastructure::ShellInterface.call(result[:metadata])
|
77
|
+
|
78
|
+
puts Podrb::Commands::Open::Output.call(result)
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "archive EPISODE_ID", "Archive the episode"
|
82
|
+
def archive(episode_id)
|
83
|
+
result = Podrb::Commands::Archive::Runner.call(episode_id)
|
84
|
+
|
85
|
+
puts Podrb::Commands::Archive::Output.call(result)
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "dearchive EPISODE_ID", "Dearchive the episode."
|
89
|
+
def dearchive(episode_id)
|
90
|
+
result = Podrb::Commands::Dearchive::Runner.call(episode_id)
|
91
|
+
|
92
|
+
puts Podrb::Commands::Dearchive::Output.call(result)
|
93
|
+
end
|
94
|
+
|
95
|
+
desc "delete PODCAST_ID", "Delete the podcast from podrb's database."
|
96
|
+
def delete(podcast_id)
|
97
|
+
result = Podrb::Commands::Delete::Runner.call(podcast_id)
|
98
|
+
|
99
|
+
puts Podrb::Commands::Delete::Output.call(result)
|
100
|
+
end
|
101
|
+
|
102
|
+
desc "sync PODCAST_ID", "Sync the podcast."
|
103
|
+
def sync(podcast_id)
|
104
|
+
result = Podrb::Commands::Sync::Runner.call(podcast_id)
|
105
|
+
|
106
|
+
puts Podrb::Commands::Sync::Output.call(result)
|
107
|
+
end
|
108
|
+
|
109
|
+
desc "update PODCAST_ID", "Update the podcast attributes."
|
110
|
+
method_option :feed, type: :string, default: "", desc: "Define the podcast feed."
|
111
|
+
def update(podcast_id)
|
112
|
+
result = Podrb::Commands::Update::Runner.call(podcast_id, options)
|
113
|
+
|
114
|
+
puts Podrb::Commands::Update::Output.call(result)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Add
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :successfully_added
|
10
|
+
<<~OUTPUT
|
11
|
+
Podcast successfully added to the database!
|
12
|
+
OUTPUT
|
13
|
+
when :already_added
|
14
|
+
<<~OUTPUT
|
15
|
+
Podcast already exists in the database.
|
16
|
+
OUTPUT
|
17
|
+
when :badly_formatted
|
18
|
+
<<~OUTPUT
|
19
|
+
The podcast feed is badly formatted or unsupported.
|
20
|
+
OUTPUT
|
21
|
+
when :missing_sync_url
|
22
|
+
<<~OUTPUT
|
23
|
+
This podcast feed doesn't provide a sync url. Please, use the --sync-url option to set this data manually.
|
24
|
+
Ex: `podrb add FEED --sync-url=SYNC_URL`
|
25
|
+
OUTPUT
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../infrastructure/feed_parser"
|
4
|
+
|
5
|
+
module Podrb
|
6
|
+
module Commands
|
7
|
+
module Add
|
8
|
+
class Runner < Podrb::Commands::BaseRunner
|
9
|
+
def call(feed, options = {})
|
10
|
+
parsed_feed = Infrastructure::FeedParser.call(feed)
|
11
|
+
parsed_options = parse_options(options)
|
12
|
+
|
13
|
+
if missing_data?(parsed_feed)
|
14
|
+
return build_failure_response(details: :badly_formatted)
|
15
|
+
end
|
16
|
+
|
17
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
18
|
+
db.transaction do
|
19
|
+
podcast = parsed_feed.podcast
|
20
|
+
podcast_feed = parsed_options["sync_url"] || podcast.feed
|
21
|
+
return build_failure_response(details: :missing_sync_url) if podcast_feed.nil?
|
22
|
+
|
23
|
+
db.execute <<-SQL
|
24
|
+
insert into podcasts
|
25
|
+
(name, description, feed, website)
|
26
|
+
values (
|
27
|
+
"#{escape_double_quotes(podcast.name)}",
|
28
|
+
"#{escape_double_quotes(podcast.description)}",
|
29
|
+
"#{escape_double_quotes(podcast_feed)}",
|
30
|
+
"#{escape_double_quotes(podcast.website)}"
|
31
|
+
);
|
32
|
+
SQL
|
33
|
+
|
34
|
+
inserted_podcast_id = db.query("select id from podcasts order by id desc limit 1;").first.id
|
35
|
+
parsed_feed.episodes.each do |e|
|
36
|
+
db.execute <<-SQL
|
37
|
+
insert into episodes
|
38
|
+
(title, release_date, podcast_id, duration, link, external_id)
|
39
|
+
values (
|
40
|
+
"#{escape_double_quotes(e.title)}",
|
41
|
+
"#{escape_double_quotes(e.release_date)}",
|
42
|
+
#{inserted_podcast_id},
|
43
|
+
"#{escape_double_quotes(e.duration)}",
|
44
|
+
"#{escape_double_quotes(e.link)}",
|
45
|
+
"#{e.external_id}"
|
46
|
+
);
|
47
|
+
SQL
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
build_success_response(details: :successfully_added)
|
52
|
+
rescue Infrastructure::Storage::Exceptions::ConstraintViolation
|
53
|
+
build_failure_response(details: :already_added)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def missing_data?(feed)
|
59
|
+
feed.podcast.nil? || feed.episodes.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
def escape_double_quotes(str)
|
63
|
+
str.gsub("\"", "\"\"")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Archive
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :episode_archived
|
10
|
+
<<~OUTPUT
|
11
|
+
Episode successfully archived!
|
12
|
+
OUTPUT
|
13
|
+
when :not_found
|
14
|
+
<<~OUTPUT
|
15
|
+
Episode not found
|
16
|
+
OUTPUT
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Archive
|
6
|
+
class Runner < Podrb::Commands::BaseRunner
|
7
|
+
def call(episode_id)
|
8
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
9
|
+
|
10
|
+
if db.query("select id from episodes where id = #{episode_id}").empty?
|
11
|
+
return build_failure_response(details: :not_found)
|
12
|
+
end
|
13
|
+
|
14
|
+
sql_code = <<~SQL
|
15
|
+
update episodes
|
16
|
+
set archived_at = '#{Time.now.iso8601}'
|
17
|
+
where id = #{episode_id};
|
18
|
+
SQL
|
19
|
+
db.execute(sql_code)
|
20
|
+
|
21
|
+
build_success_response(details: :episode_archived)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tabulo"
|
4
|
+
|
5
|
+
module Podrb
|
6
|
+
module Commands
|
7
|
+
class BaseOutput
|
8
|
+
def self.call(context = {})
|
9
|
+
new(context).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(context)
|
13
|
+
@context = context
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def generate_text_table(data:, columns:)
|
19
|
+
Tabulo::Table.new(data, *columns.map(&:to_sym), row_divider_frequency: 1).pack
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
class BaseRunner
|
6
|
+
def self.call(*args)
|
7
|
+
command = new
|
8
|
+
if command.method(:call).parameters.empty?
|
9
|
+
command.call
|
10
|
+
else
|
11
|
+
command.call(*args)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def build_success_response(details:, metadata: nil)
|
18
|
+
{status: :success, details: details, metadata: metadata}
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_failure_response(details:, metadata: nil)
|
22
|
+
{status: :failure, details: details, metadata: metadata}
|
23
|
+
end
|
24
|
+
|
25
|
+
def home_dir
|
26
|
+
ENV["HOME"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def podrb_config_dir
|
30
|
+
home_dir + "/.config/podrb"
|
31
|
+
end
|
32
|
+
|
33
|
+
def podrb_db_dir
|
34
|
+
"#{podrb_config_dir}/podrb.db"
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_options(opts)
|
38
|
+
opts.select { |k, v| v != "" && v != [] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def escape_double_quotes(str)
|
42
|
+
str.gsub("\"", "\"\"")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Dearchive
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :episode_dearchived
|
10
|
+
<<~OUTPUT
|
11
|
+
Episode successfully dearchived!
|
12
|
+
OUTPUT
|
13
|
+
when :not_found
|
14
|
+
<<~OUTPUT
|
15
|
+
Episode not found
|
16
|
+
OUTPUT
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Dearchive
|
6
|
+
class Runner < Podrb::Commands::BaseRunner
|
7
|
+
def call(episode_id)
|
8
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
9
|
+
|
10
|
+
if db.query("select id from episodes where id = #{episode_id}").empty?
|
11
|
+
return build_failure_response(details: :not_found)
|
12
|
+
end
|
13
|
+
|
14
|
+
sql_code = <<~SQL
|
15
|
+
update episodes
|
16
|
+
set archived_at = null
|
17
|
+
where id = #{episode_id};
|
18
|
+
SQL
|
19
|
+
db.execute(sql_code)
|
20
|
+
|
21
|
+
build_success_response(details: :episode_dearchived)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Delete
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :podcast_deleted
|
10
|
+
<<~OUTPUT
|
11
|
+
Podcast successfully deleted!
|
12
|
+
OUTPUT
|
13
|
+
when :not_found
|
14
|
+
<<~OUTPUT
|
15
|
+
Podcast not found
|
16
|
+
OUTPUT
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Delete
|
6
|
+
class Runner < Podrb::Commands::BaseRunner
|
7
|
+
def call(podcast_id)
|
8
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
9
|
+
|
10
|
+
if db.query("select id from podcasts where id = #{podcast_id}").empty?
|
11
|
+
return build_failure_response(details: :not_found)
|
12
|
+
end
|
13
|
+
|
14
|
+
sql_code = <<~SQL
|
15
|
+
delete from episodes
|
16
|
+
where podcast_id = #{podcast_id};
|
17
|
+
SQL
|
18
|
+
db.execute(sql_code)
|
19
|
+
|
20
|
+
sql_code = <<~SQL
|
21
|
+
delete from podcasts
|
22
|
+
where id = #{podcast_id};
|
23
|
+
SQL
|
24
|
+
db.execute(sql_code)
|
25
|
+
|
26
|
+
build_success_response(details: :podcast_deleted)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Episodes
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :records_found
|
10
|
+
text_table = generate_text_table(
|
11
|
+
data: @context[:metadata][:records],
|
12
|
+
columns: @context[:metadata][:columns]
|
13
|
+
)
|
14
|
+
<<~OUTPUT
|
15
|
+
#{text_table}
|
16
|
+
OUTPUT
|
17
|
+
when :not_found
|
18
|
+
<<~OUTPUT
|
19
|
+
No episodes was found.
|
20
|
+
OUTPUT
|
21
|
+
when :invalid_column
|
22
|
+
<<~OUTPUT
|
23
|
+
This field is invalid: #{@context[:metadata][:invalid_column]}
|
24
|
+
OUTPUT
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Podrb
|
2
|
+
module Commands
|
3
|
+
module Episodes
|
4
|
+
class Runner < Podrb::Commands::BaseRunner
|
5
|
+
ALL_COLUMNS = %w[id title release_date duration link].freeze
|
6
|
+
private_constant :ALL_COLUMNS
|
7
|
+
|
8
|
+
def call(podcast_id, options = {})
|
9
|
+
parsed_options = parse_options(options)
|
10
|
+
|
11
|
+
columns = parsed_options["fields"] || ALL_COLUMNS
|
12
|
+
sql_code = <<~SQL
|
13
|
+
select #{columns.join(", ")}
|
14
|
+
from episodes
|
15
|
+
where podcast_id = #{podcast_id}
|
16
|
+
SQL
|
17
|
+
|
18
|
+
unless parsed_options["all"]
|
19
|
+
sql_code << "and archived_at is null\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
order_by = parsed_options["order_by"] || "id"
|
23
|
+
sql_code << "order by #{order_by};\n"
|
24
|
+
|
25
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
26
|
+
records = db.query(sql_code)
|
27
|
+
|
28
|
+
build_success_response(
|
29
|
+
details: records.empty? ? :not_found : :records_found,
|
30
|
+
metadata: {records: records, columns: columns}
|
31
|
+
)
|
32
|
+
rescue Infrastructure::Storage::Exceptions::WrongSyntax => exc
|
33
|
+
cause = exc.message
|
34
|
+
if cause.include?("no such column")
|
35
|
+
invalid_column = cause.delete_prefix("no such column: ")
|
36
|
+
build_failure_response(
|
37
|
+
details: :invalid_column,
|
38
|
+
metadata: {invalid_column: invalid_column}
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Init
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :already_initialized
|
10
|
+
<<~OUTPUT
|
11
|
+
Podrb already was initialized!
|
12
|
+
OUTPUT
|
13
|
+
when :successfully_initialized
|
14
|
+
<<~OUTPUT
|
15
|
+
Podrb successfully initialized!
|
16
|
+
OUTPUT
|
17
|
+
when :home_not_found
|
18
|
+
<<~OUTPUT
|
19
|
+
It seems that $HOME is empty. Is your home directory set up correctly?
|
20
|
+
OUTPUT
|
21
|
+
when :cannot_create_initial_config
|
22
|
+
<<~OUTPUT
|
23
|
+
Podrb couldn't create the config files.
|
24
|
+
OUTPUT
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Podrb
|
6
|
+
module Commands
|
7
|
+
module Init
|
8
|
+
class Runner < Podrb::Commands::BaseRunner
|
9
|
+
def call
|
10
|
+
return build_failure_response(details: :home_not_found) if home_dir.nil?
|
11
|
+
|
12
|
+
return build_failure_response(details: :already_initialized) if Dir.exist?(podrb_config_dir)
|
13
|
+
|
14
|
+
FileUtils.mkdir_p(podrb_config_dir)
|
15
|
+
|
16
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
17
|
+
db.execute <<-SQL
|
18
|
+
create table podcasts (
|
19
|
+
id integer primary key,
|
20
|
+
name text not null,
|
21
|
+
description text,
|
22
|
+
feed text not null unique,
|
23
|
+
website text
|
24
|
+
);
|
25
|
+
SQL
|
26
|
+
db.execute <<-SQL
|
27
|
+
create table episodes (
|
28
|
+
id integer primary key,
|
29
|
+
title text not null,
|
30
|
+
release_date text,
|
31
|
+
duration text,
|
32
|
+
link text not null,
|
33
|
+
archived_at text,
|
34
|
+
external_id string unique,
|
35
|
+
podcast_id integer not null,
|
36
|
+
foreign key(podcast_id) references podcasts(id)
|
37
|
+
);
|
38
|
+
SQL
|
39
|
+
|
40
|
+
build_success_response(details: :successfully_initialized)
|
41
|
+
rescue SystemCallError, Infrastructure::Storage::Exceptions::CantStartConnection
|
42
|
+
build_failure_response(details: :cannot_create_initial_config)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Open
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :not_found
|
10
|
+
<<~OUTPUT
|
11
|
+
The episode was not found.
|
12
|
+
OUTPUT
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Podrb
|
2
|
+
module Commands
|
3
|
+
module Open
|
4
|
+
class Runner < Podrb::Commands::BaseRunner
|
5
|
+
def call(episode_id, options = {})
|
6
|
+
parsed_options = parse_options(options)
|
7
|
+
|
8
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
9
|
+
episode = db.query("select link from episodes where id = #{episode_id}")[0]
|
10
|
+
return build_failure_response(details: :not_found) if episode.nil?
|
11
|
+
|
12
|
+
browser = parsed_options["browser"] || "firefox"
|
13
|
+
cmd = "#{browser} #{episode.link}"
|
14
|
+
cmd << " && bundle exec podrb archive #{episode_id}" if parsed_options["archive"]
|
15
|
+
|
16
|
+
build_success_response(
|
17
|
+
details: :episode_found,
|
18
|
+
metadata: {cmd: cmd}
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Podcasts
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :records_found
|
10
|
+
text_table = generate_text_table(
|
11
|
+
data: @context[:metadata][:records],
|
12
|
+
columns: @context[:metadata][:columns]
|
13
|
+
)
|
14
|
+
<<~OUTPUT
|
15
|
+
#{text_table}
|
16
|
+
OUTPUT
|
17
|
+
when :empty_table
|
18
|
+
<<~OUTPUT
|
19
|
+
No podcasts yet.
|
20
|
+
OUTPUT
|
21
|
+
when :invalid_column
|
22
|
+
<<~OUTPUT
|
23
|
+
This field is invalid: #{@context[:metadata][:invalid_column]}
|
24
|
+
OUTPUT
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Podcasts
|
6
|
+
class Runner < Podrb::Commands::BaseRunner
|
7
|
+
ALL_COLUMNS = %w[id name description feed website].freeze
|
8
|
+
private_constant :ALL_COLUMNS
|
9
|
+
|
10
|
+
def call(options = {})
|
11
|
+
parsed_options = parse_options(options)
|
12
|
+
columns = parsed_options["fields"] || ALL_COLUMNS
|
13
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
14
|
+
records = db.query("select #{columns.join(", ")} from podcasts")
|
15
|
+
build_success_response(
|
16
|
+
details: records.empty? ? :empty_table : :records_found,
|
17
|
+
metadata: {records: records, columns: columns}
|
18
|
+
)
|
19
|
+
rescue Infrastructure::Storage::Exceptions::WrongSyntax => exc
|
20
|
+
cause = exc.message
|
21
|
+
if cause.include?("no such column")
|
22
|
+
invalid_column = cause.delete_prefix("no such column: ")
|
23
|
+
build_failure_response(
|
24
|
+
details: :invalid_column,
|
25
|
+
metadata: {invalid_column: invalid_column}
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Sync
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :podcast_synchronized
|
10
|
+
<<~OUTPUT
|
11
|
+
Podcast successfully synchronized!
|
12
|
+
OUTPUT
|
13
|
+
when :not_found
|
14
|
+
<<~OUTPUT
|
15
|
+
Podcast not found
|
16
|
+
OUTPUT
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../infrastructure/feed_parser"
|
4
|
+
|
5
|
+
module Podrb
|
6
|
+
module Commands
|
7
|
+
module Sync
|
8
|
+
class Runner < Podrb::Commands::BaseRunner
|
9
|
+
def call(podcast_id)
|
10
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
11
|
+
podcast = db.query("select feed from podcasts where id = #{podcast_id}").first
|
12
|
+
return build_failure_response(details: :not_found) if podcast.nil?
|
13
|
+
|
14
|
+
parsed_feed = Infrastructure::FeedParser.call(podcast.feed)
|
15
|
+
parsed_feed.episodes.each do |e|
|
16
|
+
db.execute <<-SQL
|
17
|
+
insert or ignore into episodes
|
18
|
+
(title, release_date, podcast_id, duration, link, external_id)
|
19
|
+
values (
|
20
|
+
"#{escape_double_quotes(e.title)}",
|
21
|
+
"#{escape_double_quotes(e.release_date)}",
|
22
|
+
#{podcast_id},
|
23
|
+
"#{escape_double_quotes(e.duration)}",
|
24
|
+
"#{escape_double_quotes(e.link)}",
|
25
|
+
"#{e.external_id}"
|
26
|
+
);
|
27
|
+
SQL
|
28
|
+
end
|
29
|
+
build_success_response(details: :podcast_synchronized)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Update
|
6
|
+
class Output < ::Podrb::Commands::BaseOutput
|
7
|
+
def call
|
8
|
+
case @context[:details]
|
9
|
+
when :podcast_updated
|
10
|
+
<<~OUTPUT
|
11
|
+
Podcast successfully updated!
|
12
|
+
OUTPUT
|
13
|
+
when :invalid_options
|
14
|
+
<<~OUTPUT
|
15
|
+
Invalid options. Check the documentation `podrb help update`.
|
16
|
+
OUTPUT
|
17
|
+
when :not_found
|
18
|
+
<<~OUTPUT
|
19
|
+
Podcast not found.
|
20
|
+
OUTPUT
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Podrb
|
4
|
+
module Commands
|
5
|
+
module Update
|
6
|
+
class Runner < Podrb::Commands::BaseRunner
|
7
|
+
def call(podcast_id, options = {})
|
8
|
+
parsed_options = parse_options(options)
|
9
|
+
return build_failure_response(details: :invalid_options) if parsed_options.empty?
|
10
|
+
|
11
|
+
db = Infrastructure::Storage::SQL.new(db: podrb_db_dir)
|
12
|
+
if db.query("select id from podcasts where id = #{podcast_id}").empty?
|
13
|
+
return build_failure_response(details: :not_found)
|
14
|
+
end
|
15
|
+
|
16
|
+
sql_code = <<~SQL
|
17
|
+
update podcasts
|
18
|
+
set feed = '#{options["feed"]}'
|
19
|
+
where id = #{podcast_id};
|
20
|
+
SQL
|
21
|
+
db.execute(sql_code)
|
22
|
+
build_success_response(details: :podcast_updated)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "commands/base_runner"
|
4
|
+
require_relative "commands/base_output"
|
5
|
+
require_relative "commands/add/runner"
|
6
|
+
require_relative "commands/add/output"
|
7
|
+
require_relative "commands/archive/runner"
|
8
|
+
require_relative "commands/archive/output"
|
9
|
+
require_relative "commands/dearchive/runner"
|
10
|
+
require_relative "commands/dearchive/output"
|
11
|
+
require_relative "commands/delete/runner"
|
12
|
+
require_relative "commands/delete/output"
|
13
|
+
require_relative "commands/episodes/runner"
|
14
|
+
require_relative "commands/episodes/output"
|
15
|
+
require_relative "commands/init/runner"
|
16
|
+
require_relative "commands/init/output"
|
17
|
+
require_relative "commands/open/runner"
|
18
|
+
require_relative "commands/open/output"
|
19
|
+
require_relative "commands/podcasts/runner"
|
20
|
+
require_relative "commands/podcasts/output"
|
21
|
+
require_relative "commands/sync/runner"
|
22
|
+
require_relative "commands/sync/output"
|
23
|
+
require_relative "commands/update/runner"
|
24
|
+
require_relative "commands/update/output"
|
25
|
+
|
26
|
+
module Podrb
|
27
|
+
module Commands; end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Infrastructure
|
4
|
+
class DTO
|
5
|
+
def initialize(attrs)
|
6
|
+
@attrs = attrs
|
7
|
+
end
|
8
|
+
|
9
|
+
def respond_to_missing?(symbol, include_private = false)
|
10
|
+
@attrs.key?(symbol) || super
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(symbol, *args)
|
14
|
+
@attrs[symbol]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "feedjira"
|
5
|
+
|
6
|
+
require_relative "dto"
|
7
|
+
|
8
|
+
module Infrastructure
|
9
|
+
module FeedParser
|
10
|
+
def self.call(feed)
|
11
|
+
content = if File.exist?(feed)
|
12
|
+
File.new(feed).read
|
13
|
+
else
|
14
|
+
Net::HTTP.get(URI(feed))
|
15
|
+
end
|
16
|
+
parsed_content = Feedjira.parse(content)
|
17
|
+
|
18
|
+
podcast = Infrastructure::DTO.new(
|
19
|
+
name: parsed_content.title,
|
20
|
+
description: parsed_content.description,
|
21
|
+
feed: parsed_content.itunes_new_feed_url,
|
22
|
+
website: parsed_content.url
|
23
|
+
)
|
24
|
+
|
25
|
+
episodes = parsed_content.entries.map do |e|
|
26
|
+
Infrastructure::DTO.new(
|
27
|
+
title: e.title,
|
28
|
+
release_date: e.published.iso8601,
|
29
|
+
duration: e.itunes_duration,
|
30
|
+
link: e.url,
|
31
|
+
external_id: e.entry_id
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The #reverse is necessary to put the oldest episodes on the top of the feed.
|
36
|
+
Infrastructure::DTO.new(podcast: podcast, episodes: episodes.reverse)
|
37
|
+
rescue NoMethodError
|
38
|
+
Infrastructure::DTO.new(podcast: nil, episodes: nil)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sqlite3"
|
4
|
+
require_relative "../dto"
|
5
|
+
|
6
|
+
module Infrastructure
|
7
|
+
module Storage
|
8
|
+
class SQL
|
9
|
+
def initialize(db:)
|
10
|
+
@conn = SQLite3::Database.new(db)
|
11
|
+
rescue SQLite3::CantOpenException
|
12
|
+
raise Exceptions::CantStartConnection
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(sql)
|
16
|
+
result = @conn.execute(sql)
|
17
|
+
|
18
|
+
# SQLite3 #execute always return an Array when the
|
19
|
+
# statement was successfully executed, and sometimes,
|
20
|
+
# the array will be empty, which isn't useful for us.
|
21
|
+
result.empty? ? true : result
|
22
|
+
rescue SQLite3::SQLException => exc
|
23
|
+
raise Exceptions::WrongSyntax, exc.message
|
24
|
+
rescue SQLite3::ConstraintException => exc
|
25
|
+
raise Exceptions::ConstraintViolation, exc.message
|
26
|
+
end
|
27
|
+
|
28
|
+
def query(sql)
|
29
|
+
parsed_result = []
|
30
|
+
|
31
|
+
@conn.query(sql) do |result|
|
32
|
+
result.each_hash do |row|
|
33
|
+
parsed_result << Infrastructure::DTO.new(row.transform_keys(&:to_sym))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
parsed_result
|
38
|
+
rescue SQLite3::SQLException => exc
|
39
|
+
raise Exceptions::WrongSyntax, exc.message
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reference
|
43
|
+
# https://github.com/sparklemotion/sqlite3-ruby/blob/v1.5.4/lib/sqlite3/database.rb#L632
|
44
|
+
def transaction(mode = :deferred, &block)
|
45
|
+
@conn.transaction(mode, &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module Exceptions
|
50
|
+
class Exception < ::StandardError; end
|
51
|
+
|
52
|
+
class CantStartConnection < Exception; end
|
53
|
+
|
54
|
+
class WrongSyntax < Exception; end
|
55
|
+
|
56
|
+
class ConstraintViolation < Exception; end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/podrb.rb
ADDED
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: podrb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gustavo Ribeiro
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: feedjira
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.5'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tabulo
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.8'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.8'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- grdev@tutanota.com
|
72
|
+
executables:
|
73
|
+
- podrb
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- exe/podrb
|
78
|
+
- lib/podrb.rb
|
79
|
+
- lib/podrb/cli.rb
|
80
|
+
- lib/podrb/commands.rb
|
81
|
+
- lib/podrb/commands/add/output.rb
|
82
|
+
- lib/podrb/commands/add/runner.rb
|
83
|
+
- lib/podrb/commands/archive/output.rb
|
84
|
+
- lib/podrb/commands/archive/runner.rb
|
85
|
+
- lib/podrb/commands/base_output.rb
|
86
|
+
- lib/podrb/commands/base_runner.rb
|
87
|
+
- lib/podrb/commands/dearchive/output.rb
|
88
|
+
- lib/podrb/commands/dearchive/runner.rb
|
89
|
+
- lib/podrb/commands/delete/output.rb
|
90
|
+
- lib/podrb/commands/delete/runner.rb
|
91
|
+
- lib/podrb/commands/episodes/output.rb
|
92
|
+
- lib/podrb/commands/episodes/runner.rb
|
93
|
+
- lib/podrb/commands/init/output.rb
|
94
|
+
- lib/podrb/commands/init/runner.rb
|
95
|
+
- lib/podrb/commands/open/output.rb
|
96
|
+
- lib/podrb/commands/open/runner.rb
|
97
|
+
- lib/podrb/commands/podcasts/output.rb
|
98
|
+
- lib/podrb/commands/podcasts/runner.rb
|
99
|
+
- lib/podrb/commands/sync/output.rb
|
100
|
+
- lib/podrb/commands/sync/runner.rb
|
101
|
+
- lib/podrb/commands/update/output.rb
|
102
|
+
- lib/podrb/commands/update/runner.rb
|
103
|
+
- lib/podrb/infrastructure/dto.rb
|
104
|
+
- lib/podrb/infrastructure/feed_parser.rb
|
105
|
+
- lib/podrb/infrastructure/shell_interface.rb
|
106
|
+
- lib/podrb/infrastructure/storage/sql.rb
|
107
|
+
- lib/podrb/version.rb
|
108
|
+
homepage: https://github.com/gustavothecoder/podrb
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata:
|
112
|
+
homepage_uri: https://github.com/gustavothecoder/podrb
|
113
|
+
source_code_uri: https://github.com/gustavothecoder/podrb
|
114
|
+
changelog_uri: https://github.com/gustavothecoder/podrb/blob/main/CHANGELOG.md
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 2.7.0
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubygems_version: 3.1.6
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Minimalist CLI to manage podcasts
|
134
|
+
test_files: []
|