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 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "podrb"
4
+
5
+ Podrb::CLI.start
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infrastructure
4
+ module ShellInterface
5
+ def self.call(args)
6
+ return if args.nil? || args[:cmd].nil?
7
+
8
+ `#{args[:cmd]}`
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Podrb
4
+ VERSION = "0.1.0"
5
+ end
data/lib/podrb.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "podrb/version"
4
+ require_relative "podrb/cli"
5
+
6
+ module Podrb
7
+ class Error < StandardError; end
8
+ end
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: []