podrb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []