podrb 0.1.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 +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: []
|