good_audible_story_sync 0.0.5
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/LICENSE +21 -0
- data/README.md +113 -0
- data/bin/good-audible-story-sync +6 -0
- data/lib/good_audible_story_sync/audible/auth.rb +262 -0
- data/lib/good_audible_story_sync/audible/auth_flow.rb +70 -0
- data/lib/good_audible_story_sync/audible/client.rb +220 -0
- data/lib/good_audible_story_sync/audible/library.rb +318 -0
- data/lib/good_audible_story_sync/audible/library_item.rb +213 -0
- data/lib/good_audible_story_sync/audible/user_profile.rb +39 -0
- data/lib/good_audible_story_sync/audible.rb +13 -0
- data/lib/good_audible_story_sync/database/audible_books.rb +66 -0
- data/lib/good_audible_story_sync/database/client.rb +70 -0
- data/lib/good_audible_story_sync/database/credentials.rb +48 -0
- data/lib/good_audible_story_sync/database/goodreads_books.rb +60 -0
- data/lib/good_audible_story_sync/database/storygraph_books.rb +74 -0
- data/lib/good_audible_story_sync/database/sync_times.rb +52 -0
- data/lib/good_audible_story_sync/database.rb +16 -0
- data/lib/good_audible_story_sync/goodreads/auth.rb +137 -0
- data/lib/good_audible_story_sync/goodreads/auth_flow.rb +70 -0
- data/lib/good_audible_story_sync/goodreads/book.rb +171 -0
- data/lib/good_audible_story_sync/goodreads/client.rb +98 -0
- data/lib/good_audible_story_sync/goodreads/library.rb +149 -0
- data/lib/good_audible_story_sync/goodreads.rb +12 -0
- data/lib/good_audible_story_sync/input_loop.rb +214 -0
- data/lib/good_audible_story_sync/options.rb +70 -0
- data/lib/good_audible_story_sync/storygraph/auth.rb +91 -0
- data/lib/good_audible_story_sync/storygraph/auth_flow.rb +70 -0
- data/lib/good_audible_story_sync/storygraph/book.rb +261 -0
- data/lib/good_audible_story_sync/storygraph/client.rb +247 -0
- data/lib/good_audible_story_sync/storygraph/library.rb +183 -0
- data/lib/good_audible_story_sync/storygraph/look_up_book_flow.rb +172 -0
- data/lib/good_audible_story_sync/storygraph/mark_finished_flow.rb +201 -0
- data/lib/good_audible_story_sync/storygraph.rb +14 -0
- data/lib/good_audible_story_sync/util/cipher.rb +43 -0
- data/lib/good_audible_story_sync/util/keychain.rb +32 -0
- data/lib/good_audible_story_sync/util.rb +92 -0
- data/lib/good_audible_story_sync/version.rb +6 -0
- data/lib/good_audible_story_sync.rb +14 -0
- metadata +80 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
|
|
4
|
+
module GoodAudibleStorySync
|
|
5
|
+
module Storygraph
|
|
6
|
+
class MarkFinishedFlow
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
class UserCommand < T::Enum
|
|
10
|
+
enums do
|
|
11
|
+
SetReadDate = new("r")
|
|
12
|
+
NextBook = new("n")
|
|
13
|
+
Cancel = new("c")
|
|
14
|
+
Quit = new("q")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig do
|
|
19
|
+
params(
|
|
20
|
+
audible_library: Audible::Library,
|
|
21
|
+
library: Library,
|
|
22
|
+
client: Client,
|
|
23
|
+
db_client: Database::Client
|
|
24
|
+
).void
|
|
25
|
+
end
|
|
26
|
+
def self.run(audible_library:, library:, client:, db_client:)
|
|
27
|
+
new(audible_library: audible_library, library: library, client: client,
|
|
28
|
+
db_client: db_client).run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig do
|
|
32
|
+
params(
|
|
33
|
+
audible_library: Audible::Library,
|
|
34
|
+
library: Library,
|
|
35
|
+
client: Client,
|
|
36
|
+
db_client: Database::Client
|
|
37
|
+
).void
|
|
38
|
+
end
|
|
39
|
+
def initialize(audible_library:, library:, client:, db_client:)
|
|
40
|
+
@audible_library = audible_library
|
|
41
|
+
@library = library
|
|
42
|
+
@client = client
|
|
43
|
+
@db_client = db_client
|
|
44
|
+
@current_book = T.let(nil, T.nilable(Book))
|
|
45
|
+
@current_finish_date = T.let(nil, T.nilable(Date))
|
|
46
|
+
@stop_marking_finished = T.let(false, T::Boolean)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { void }
|
|
50
|
+
def run
|
|
51
|
+
finish_dates_by_isbn.each do |isbn, finish_date|
|
|
52
|
+
@current_finish_date = finish_date
|
|
53
|
+
process_book(isbn)
|
|
54
|
+
break if @stop_marking_finished
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
sig { params(isbn: String).void }
|
|
61
|
+
def process_book(isbn)
|
|
62
|
+
@current_book = find_book_by_isbn(isbn)
|
|
63
|
+
return unless @current_book
|
|
64
|
+
|
|
65
|
+
storygraph_finish_date = @current_book.finished_on
|
|
66
|
+
title_and_author = @current_book.title_and_author(stylize: true)
|
|
67
|
+
|
|
68
|
+
if storygraph_finish_date.nil?
|
|
69
|
+
puts "#{Util::INFO_EMOJI} Storygraph book #{title_and_author} has no finish date"
|
|
70
|
+
prompt_user_about_current_book
|
|
71
|
+
elsif storygraph_finish_date == @current_finish_date
|
|
72
|
+
puts "#{Util::SUCCESS_EMOJI} Storygraph book #{title_and_author} already " \
|
|
73
|
+
"marked as finished on #{Util.pretty_date(@current_finish_date)}"
|
|
74
|
+
else
|
|
75
|
+
puts "#{Util::WARNING_EMOJI} #{title_and_author}"
|
|
76
|
+
puts "#{Util::TAB}Storygraph finish date: #{Util.pretty_date(storygraph_finish_date)}"
|
|
77
|
+
puts "#{Util::TAB}Versus Audible: #{Util.pretty_date(T.must(@current_finish_date))}"
|
|
78
|
+
prompt_user_about_current_book
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(isbn: String).returns(T.nilable(Book)) }
|
|
83
|
+
def find_book_by_isbn(isbn)
|
|
84
|
+
audible_book = @audible_library.find_by_isbn(isbn)
|
|
85
|
+
if audible_book
|
|
86
|
+
puts "#{Util::INFO_EMOJI}Looking up #{audible_book.to_s(stylize: true)} on Storygraph..."
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Do we already have the book associated with the ISBN in the local database?
|
|
90
|
+
book = @library.find_by_isbn(isbn)
|
|
91
|
+
|
|
92
|
+
unless book
|
|
93
|
+
# If not, search for it on Storygraph using the ISBN, then by title and author
|
|
94
|
+
book = @client.find_by_isbn(isbn, fallback_query: audible_book&.search_query)
|
|
95
|
+
|
|
96
|
+
if book
|
|
97
|
+
# Associate the book with its ISBN in the local library database
|
|
98
|
+
@library.add_book(book)
|
|
99
|
+
book.save_to_database(@db_client.storygraph_books)
|
|
100
|
+
else
|
|
101
|
+
puts "#{Util::WARNING_EMOJI} Book with ISBN #{isbn} not found on Storygraph"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
book
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { returns UserCommand }
|
|
109
|
+
def get_user_command
|
|
110
|
+
cmd = T.let(nil, T.nilable(UserCommand))
|
|
111
|
+
while cmd.nil?
|
|
112
|
+
print "Choose an option: "
|
|
113
|
+
input = gets.chomp
|
|
114
|
+
cmd = UserCommand.try_deserialize(input)
|
|
115
|
+
puts "Invalid command" if cmd.nil?
|
|
116
|
+
end
|
|
117
|
+
cmd
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { void }
|
|
121
|
+
def prompt_user_about_current_book
|
|
122
|
+
book = T.must(@current_book)
|
|
123
|
+
puts book.to_s(stylize: true)
|
|
124
|
+
print_options
|
|
125
|
+
cmd = get_user_command
|
|
126
|
+
process_command(cmd)
|
|
127
|
+
puts
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { params(cmd: UserCommand).void }
|
|
131
|
+
def process_command(cmd)
|
|
132
|
+
case cmd
|
|
133
|
+
when UserCommand::Quit then quit
|
|
134
|
+
when UserCommand::NextBook then skip_current_book
|
|
135
|
+
when UserCommand::SetReadDate then set_read_date_on_current_book
|
|
136
|
+
when UserCommand::Cancel then cancel
|
|
137
|
+
else
|
|
138
|
+
T.absurd(cmd)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { void }
|
|
143
|
+
def cancel
|
|
144
|
+
@stop_marking_finished = true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sig { returns T::Boolean }
|
|
148
|
+
def set_read_date_on_current_book
|
|
149
|
+
book = T.must(@current_book)
|
|
150
|
+
book_id = book.id
|
|
151
|
+
unless book_id
|
|
152
|
+
puts "#{Util::WARNING_EMOJI} Book #{book.title_and_author(stylize: true)} has no " \
|
|
153
|
+
"Storygraph ID, cannot set read date"
|
|
154
|
+
return false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
finish_date = T.must(@current_finish_date)
|
|
158
|
+
success = @client.set_read_date(book_id, finish_date)
|
|
159
|
+
|
|
160
|
+
if success
|
|
161
|
+
book.finished_on = finish_date
|
|
162
|
+
book.save_to_database(@db_client.storygraph_books)
|
|
163
|
+
puts "#{Util::TAB}#{Util::SUCCESS_EMOJI} Done! #{book.url(stylize: true)}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
success
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
sig { void }
|
|
170
|
+
def skip_current_book
|
|
171
|
+
puts "#{Util::TAB}#{Util::INFO_EMOJI} Skipping..."
|
|
172
|
+
@current_book = nil
|
|
173
|
+
@current_finish_date = nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
sig { void }
|
|
177
|
+
def quit
|
|
178
|
+
puts "Goodbye!"
|
|
179
|
+
exit 0
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
sig { returns T::Hash[String, Date] }
|
|
183
|
+
def finish_dates_by_isbn
|
|
184
|
+
@finish_dates_by_isbn ||= @audible_library.finish_dates_by_isbn
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
sig { void }
|
|
188
|
+
def print_options
|
|
189
|
+
print_option(UserCommand::SetReadDate, "set read date to #{Util.pretty_date(T.must(@current_finish_date))}")
|
|
190
|
+
print_option(UserCommand::NextBook, "next book")
|
|
191
|
+
print_option(UserCommand::Cancel, "cancel")
|
|
192
|
+
print_option(UserCommand::Quit, "quit")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sig { params(option: UserCommand, description: String).void }
|
|
196
|
+
def print_option(option, description)
|
|
197
|
+
Util.print_option(option.serialize, description)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodAudibleStorySync
|
|
4
|
+
module Storygraph
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "storygraph/auth"
|
|
9
|
+
require_relative "storygraph/auth_flow"
|
|
10
|
+
require_relative "storygraph/book"
|
|
11
|
+
require_relative "storygraph/client"
|
|
12
|
+
require_relative "storygraph/library"
|
|
13
|
+
require_relative "storygraph/look_up_book_flow"
|
|
14
|
+
require_relative "storygraph/mark_finished_flow"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
|
|
4
|
+
require "lockbox"
|
|
5
|
+
require_relative "./keychain"
|
|
6
|
+
|
|
7
|
+
module GoodAudibleStorySync
|
|
8
|
+
module Util
|
|
9
|
+
class Cipher
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
ENCRYPTION_KEY_NAME = "good_audible_story_sync_encryption_key"
|
|
13
|
+
|
|
14
|
+
sig { returns String }
|
|
15
|
+
def self.key
|
|
16
|
+
result = Keychain.load(name: ENCRYPTION_KEY_NAME)
|
|
17
|
+
if result.nil? || result.empty?
|
|
18
|
+
puts "#{INFO_EMOJI} No encryption key found in keychain. Generating a new one..."
|
|
19
|
+
result = Lockbox.generate_key
|
|
20
|
+
Keychain.save(name: ENCRYPTION_KEY_NAME, value: result)
|
|
21
|
+
else
|
|
22
|
+
puts "#{INFO_EMOJI} Using GoodAudibleStorySync encryption key from keychain"
|
|
23
|
+
end
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { void }
|
|
28
|
+
def initialize
|
|
29
|
+
@lockbox = Lockbox.new(key: self.class.key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { params(contents: String).returns(String) }
|
|
33
|
+
def encrypt(contents)
|
|
34
|
+
@lockbox.encrypt(contents)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(value: String).returns(String) }
|
|
38
|
+
def decrypt(value)
|
|
39
|
+
@lockbox.decrypt(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
# encoding: utf-8
|
|
4
|
+
|
|
5
|
+
module GoodAudibleStorySync
|
|
6
|
+
module Util
|
|
7
|
+
class Keychain
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
LOCK_EMOJI = "🔐"
|
|
11
|
+
|
|
12
|
+
sig { params(name: String, value: String).void }
|
|
13
|
+
def self.save(name:, value:)
|
|
14
|
+
account_name = self.account_name
|
|
15
|
+
puts "#{LOCK_EMOJI} Saving '#{name}' to #{account_name}'s keychain..."
|
|
16
|
+
`security add-generic-password -s '#{name}' -a '#{account_name}' -w '#{value}'`
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
|
20
|
+
def self.load(name:)
|
|
21
|
+
account_name = self.account_name
|
|
22
|
+
puts "#{LOCK_EMOJI} Looking for '#{name}' in #{account_name}'s keychain..."
|
|
23
|
+
`security find-generic-password -w -s '#{name}' -a '#{account_name}'`.strip
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns String }
|
|
27
|
+
def self.account_name
|
|
28
|
+
`whoami`.strip
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
# encoding: utf-8
|
|
4
|
+
|
|
5
|
+
require "rainbow"
|
|
6
|
+
|
|
7
|
+
module GoodAudibleStorySync
|
|
8
|
+
module Util
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
TAB = " "
|
|
12
|
+
INFO_EMOJI = "ℹ️"
|
|
13
|
+
ERROR_EMOJI = "❌"
|
|
14
|
+
SAVE_EMOJI = "💾"
|
|
15
|
+
SUCCESS_EMOJI = "🟢"
|
|
16
|
+
DONE_EMOJI = "✅"
|
|
17
|
+
NEWLINE_EMOJI = "⮑"
|
|
18
|
+
WARNING_EMOJI = "🟡"
|
|
19
|
+
|
|
20
|
+
sig { params(words: T::Array[String]).returns(String) }
|
|
21
|
+
def self.join_words(words)
|
|
22
|
+
case words.size
|
|
23
|
+
when 0 then ""
|
|
24
|
+
when 1 then T.must(words[0])
|
|
25
|
+
when 2 then words.join(" and ")
|
|
26
|
+
else
|
|
27
|
+
head = T.must(words[0...-1]).join(", ")
|
|
28
|
+
tail = T.must(words[-1])
|
|
29
|
+
"#{head}, and #{tail}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(words_str: String).returns(T::Array[String]) }
|
|
34
|
+
def self.split_words(words_str)
|
|
35
|
+
words_str.split(/, | and/).map { |word| word.strip.sub(/^and /, "") }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { params(timestamp: T.any(DateTime, Time)).returns(String) }
|
|
39
|
+
def self.pretty_time(timestamp)
|
|
40
|
+
# e.g., "Fri November 29, 2024 at 2:47am"
|
|
41
|
+
timestamp.strftime("%a %B %-d, %Y at %-l:%M%P")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(date: T.any(DateTime, Time, Date)).returns(String) }
|
|
45
|
+
def self.pretty_date(date)
|
|
46
|
+
# e.g., "Fri November 29, 2024"
|
|
47
|
+
date.strftime("%a %B %-d, %Y")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(str: T.nilable(String)).returns(T.nilable(String)) }
|
|
51
|
+
def self.squish(str)
|
|
52
|
+
return unless str
|
|
53
|
+
str.gsub(/[[:space:]]+/, " ").strip
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(str: String).returns(T::Boolean) }
|
|
57
|
+
def self.integer?(str)
|
|
58
|
+
str.to_i.to_s == str
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(str: String).returns(T::Boolean) }
|
|
62
|
+
def self.isbn?(str)
|
|
63
|
+
idx = str =~ /^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$/
|
|
64
|
+
idx == 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(option: String, description: String).void }
|
|
68
|
+
def self.print_option(option, description)
|
|
69
|
+
desc_words = description.split(" ")
|
|
70
|
+
word_to_highlight = desc_words.detect { |word| word.downcase.start_with?(option) }
|
|
71
|
+
highlighted_word_index = desc_words.index(word_to_highlight)
|
|
72
|
+
highlighted_option = Rainbow(option).green
|
|
73
|
+
highlighted_word = if word_to_highlight
|
|
74
|
+
head = word_to_highlight.slice(0)
|
|
75
|
+
tail = word_to_highlight.slice(1..)
|
|
76
|
+
highlighted_head = Rainbow(head).green
|
|
77
|
+
"#{highlighted_head}#{tail}"
|
|
78
|
+
end
|
|
79
|
+
highlighted_description = if highlighted_word_index
|
|
80
|
+
head = (desc_words.slice(0, highlighted_word_index) || []).join(" ")
|
|
81
|
+
tail = (desc_words.slice(highlighted_word_index + 1..) || []).join(" ")
|
|
82
|
+
[head, highlighted_word, tail].compact.join(" ").strip
|
|
83
|
+
else
|
|
84
|
+
description
|
|
85
|
+
end
|
|
86
|
+
puts "#{highlighted_option}) #{highlighted_description}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
require_relative "util/cipher"
|
|
92
|
+
require_relative "util/keychain"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sorbet-runtime"
|
|
4
|
+
|
|
5
|
+
module GoodAudibleStorySync
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "good_audible_story_sync/audible"
|
|
9
|
+
require_relative "good_audible_story_sync/database"
|
|
10
|
+
require_relative "good_audible_story_sync/goodreads"
|
|
11
|
+
require_relative "good_audible_story_sync/input_loop"
|
|
12
|
+
require_relative "good_audible_story_sync/options"
|
|
13
|
+
require_relative "good_audible_story_sync/storygraph"
|
|
14
|
+
require_relative "good_audible_story_sync/util"
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: good_audible_story_sync
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sarah Vessels
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Interactive script to mark books as finished as well as set the finish
|
|
13
|
+
date on Storygraph, based on your Audible activity.
|
|
14
|
+
email: cheshire137@gmail.com
|
|
15
|
+
executables:
|
|
16
|
+
- good-audible-story-sync
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- bin/good-audible-story-sync
|
|
23
|
+
- lib/good_audible_story_sync.rb
|
|
24
|
+
- lib/good_audible_story_sync/audible.rb
|
|
25
|
+
- lib/good_audible_story_sync/audible/auth.rb
|
|
26
|
+
- lib/good_audible_story_sync/audible/auth_flow.rb
|
|
27
|
+
- lib/good_audible_story_sync/audible/client.rb
|
|
28
|
+
- lib/good_audible_story_sync/audible/library.rb
|
|
29
|
+
- lib/good_audible_story_sync/audible/library_item.rb
|
|
30
|
+
- lib/good_audible_story_sync/audible/user_profile.rb
|
|
31
|
+
- lib/good_audible_story_sync/database.rb
|
|
32
|
+
- lib/good_audible_story_sync/database/audible_books.rb
|
|
33
|
+
- lib/good_audible_story_sync/database/client.rb
|
|
34
|
+
- lib/good_audible_story_sync/database/credentials.rb
|
|
35
|
+
- lib/good_audible_story_sync/database/goodreads_books.rb
|
|
36
|
+
- lib/good_audible_story_sync/database/storygraph_books.rb
|
|
37
|
+
- lib/good_audible_story_sync/database/sync_times.rb
|
|
38
|
+
- lib/good_audible_story_sync/goodreads.rb
|
|
39
|
+
- lib/good_audible_story_sync/goodreads/auth.rb
|
|
40
|
+
- lib/good_audible_story_sync/goodreads/auth_flow.rb
|
|
41
|
+
- lib/good_audible_story_sync/goodreads/book.rb
|
|
42
|
+
- lib/good_audible_story_sync/goodreads/client.rb
|
|
43
|
+
- lib/good_audible_story_sync/goodreads/library.rb
|
|
44
|
+
- lib/good_audible_story_sync/input_loop.rb
|
|
45
|
+
- lib/good_audible_story_sync/options.rb
|
|
46
|
+
- lib/good_audible_story_sync/storygraph.rb
|
|
47
|
+
- lib/good_audible_story_sync/storygraph/auth.rb
|
|
48
|
+
- lib/good_audible_story_sync/storygraph/auth_flow.rb
|
|
49
|
+
- lib/good_audible_story_sync/storygraph/book.rb
|
|
50
|
+
- lib/good_audible_story_sync/storygraph/client.rb
|
|
51
|
+
- lib/good_audible_story_sync/storygraph/library.rb
|
|
52
|
+
- lib/good_audible_story_sync/storygraph/look_up_book_flow.rb
|
|
53
|
+
- lib/good_audible_story_sync/storygraph/mark_finished_flow.rb
|
|
54
|
+
- lib/good_audible_story_sync/util.rb
|
|
55
|
+
- lib/good_audible_story_sync/util/cipher.rb
|
|
56
|
+
- lib/good_audible_story_sync/util/keychain.rb
|
|
57
|
+
- lib/good_audible_story_sync/version.rb
|
|
58
|
+
homepage: https://github.com/cheshire137/good-audible-story-sync
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata: {}
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.6.9
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Command-line tool to sync your read books from Audible to Storygraph and,
|
|
79
|
+
eventually, Goodreads.
|
|
80
|
+
test_files: []
|