appydave-tools 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -7
- data/CHANGELOG.md +20 -0
- data/bin/bank_reconciliation.rb +30 -5
- data/bin/subtitle_master.rb +113 -0
- data/bin/youtube_manager.rb +73 -0
- data/lib/appydave/tools/bank_reconciliation/clean/clean_transactions.rb +17 -6
- data/lib/appydave/tools/bank_reconciliation/clean/mapper.rb +28 -2
- data/lib/appydave/tools/bank_reconciliation/clean/read_transactions.rb +26 -0
- data/lib/appydave/tools/bank_reconciliation/models/transaction.rb +36 -0
- data/lib/appydave/tools/indifferent_access_hash.rb +41 -0
- data/lib/appydave/tools/subtitle_master/_doc.md +21 -0
- data/lib/appydave/tools/subtitle_master/clean.rb +80 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools/youtube_manager/authorization.rb +51 -0
- data/lib/appydave/tools/youtube_manager/get_video.rb +46 -0
- data/lib/appydave/tools/youtube_manager/reports/video_content_report.rb +27 -0
- data/lib/appydave/tools/youtube_manager/reports/video_details_report.rb +36 -0
- data/lib/appydave/tools/youtube_manager/youtube_base.rb +16 -0
- data/lib/appydave/tools.rb +17 -0
- data/package-lock.json +2 -2
- data/package.json +1 -1
- metadata +54 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ba996537dc1739ee3fb48dc8d7998d6162a5abf049a14532ca099061771f368
|
4
|
+
data.tar.gz: 725cfcaec333d10f74efcf249d4b06e1c671ba00f873ffdb86b8450db9b7ac0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d63ebd803779eee9bb7715911492ced07dd48b373bd541a6eef0106737374025919611bc0a00de1ef003dec98a9b17417236bcf1784c50a9cc81f3cc21d5d660
|
7
|
+
data.tar.gz: b191193647bdc0196fbbc47db9dde26f6dfa0e65f1d1d709aff7befbf1aa1ac0d17438bab94643f24860a177060862d86060bea9bcdc0a2ea54c46fb7d26171c
|
data/.rubocop.yml
CHANGED
@@ -43,8 +43,6 @@ Metrics/BlockLength:
|
|
43
43
|
RSpec/ExampleLength:
|
44
44
|
Max: 25
|
45
45
|
|
46
|
-
Metrics/MethodLength:
|
47
|
-
Max: 25
|
48
46
|
|
49
47
|
Layout/LineLength:
|
50
48
|
Max: 200
|
@@ -77,6 +75,8 @@ Naming/MethodParameterName:
|
|
77
75
|
Style/EmptyMethod:
|
78
76
|
Exclude:
|
79
77
|
- "**/spec/**/*"
|
78
|
+
Style/EmptyFile:
|
79
|
+
Enabled: false
|
80
80
|
Metrics/ParameterLists:
|
81
81
|
Exclude:
|
82
82
|
- "**/spec/**/*"
|
@@ -117,16 +117,27 @@ RSpec/DescribeClass:
|
|
117
117
|
RSpec/PendingWithoutReason:
|
118
118
|
Enabled: false
|
119
119
|
|
120
|
+
RSpec/MultipleMemoizedHelpers:
|
121
|
+
Enabled: false
|
122
|
+
|
120
123
|
Metrics/AbcSize:
|
121
124
|
Max: 25
|
122
125
|
Exclude:
|
123
126
|
- "bin/*"
|
127
|
+
- "**/spec/**/*"
|
128
|
+
- "lib/appydave/**/*.rb"
|
124
129
|
Metrics/CyclomaticComplexity:
|
125
130
|
Exclude:
|
126
|
-
- "**/
|
127
|
-
- "lib/appydave
|
131
|
+
- "**/spec/**/*"
|
132
|
+
- "lib/appydave/**/*.rb"
|
128
133
|
Metrics/PerceivedComplexity:
|
129
134
|
Exclude:
|
130
|
-
- "**/
|
131
|
-
|
132
|
-
|
135
|
+
- "**/spec/**/*"
|
136
|
+
- "lib/appydave/**/*.rb"
|
137
|
+
|
138
|
+
Metrics/MethodLength:
|
139
|
+
Max: 25
|
140
|
+
Exclude:
|
141
|
+
- "**/spec/**/*"
|
142
|
+
- "bin/*.rb"
|
143
|
+
- "lib/appydave/**/*.rb"
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
# [0.8.0](https://github.com/klueless-io/appydave-tools/compare/v0.7.0...v0.8.0) (2024-06-06)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* update bank reconciliation components ([0f5dea1](https://github.com/klueless-io/appydave-tools/commit/0f5dea1f75baced1458619ead3fc5bd5bc69e0d5))
|
7
|
+
|
8
|
+
|
9
|
+
### Features
|
10
|
+
|
11
|
+
* subtitle master for cleaning the SRT files created by whisper AI ([9f7bd37](https://github.com/klueless-io/appydave-tools/commit/9f7bd3795f4361e0615307874a88370ab49f97a7))
|
12
|
+
* subtitle master for cleaning the SRT files created by whisper AI ([0475a9e](https://github.com/klueless-io/appydave-tools/commit/0475a9ec07f2735e52ce54db6afa96e30e00c0e6))
|
13
|
+
|
14
|
+
# [0.7.0](https://github.com/klueless-io/appydave-tools/compare/v0.6.1...v0.7.0) (2024-05-29)
|
15
|
+
|
16
|
+
|
17
|
+
### Features
|
18
|
+
|
19
|
+
* new tool for doing bank reconciliations with chart of account matching ([9b82605](https://github.com/klueless-io/appydave-tools/commit/9b8260571f6046470d5963354ee1c80e493a0f28))
|
20
|
+
|
1
21
|
## [0.6.1](https://github.com/klueless-io/appydave-tools/compare/v0.6.0...v0.6.1) (2024-05-26)
|
2
22
|
|
3
23
|
|
data/bin/bank_reconciliation.rb
CHANGED
@@ -32,19 +32,42 @@ class BankReconciliationCLI
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def clean_transactions(args)
|
35
|
-
options = {}
|
35
|
+
options = { include: [] }
|
36
36
|
OptionParser.new do |opts|
|
37
37
|
opts.banner = 'Usage: bank_reconciliation.rb clean [options]'
|
38
|
-
|
39
|
-
opts.on('-
|
40
|
-
|
38
|
+
|
39
|
+
opts.on('-i', '--include PATTERN', 'GLOB pattern for source transaction files') do |v|
|
40
|
+
options[:include] << v
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-f', '--transaction FOLDER', 'Transaction CSV folder where original banking CSV files are stored') do |v|
|
44
|
+
options[:transaction_folder] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('-o', '--output FILE', 'Output CSV file name') do |v|
|
48
|
+
options[:output] = v
|
49
|
+
end
|
50
|
+
|
41
51
|
opts.on_tail('-h', '--help', 'Show this message') do
|
42
52
|
puts opts
|
43
53
|
exit
|
44
54
|
end
|
45
55
|
end.parse!(args)
|
46
56
|
|
47
|
-
|
57
|
+
transaction_folder = options[:transaction_folder] || '/default/transaction/folder'
|
58
|
+
output_file = options[:output] || 'clean_transactions.csv'
|
59
|
+
include_patterns = options[:include].empty? ? ['*'] : options[:include]
|
60
|
+
|
61
|
+
puts "Cleaning transactions with options: #{options}"
|
62
|
+
|
63
|
+
# Ensure the clean directory exists
|
64
|
+
clean_dir = File.dirname(output_file)
|
65
|
+
FileUtils.mkdir_p(clean_dir)
|
66
|
+
|
67
|
+
# Initialize the CleanTransactions class and process the files
|
68
|
+
cleaner = Appydave::Tools::BankReconciliation::Clean::CleanTransactions.new(transaction_folder: transaction_folder)
|
69
|
+
cleaner.clean_transactions(include_patterns, output_file)
|
70
|
+
|
48
71
|
puts "Cleaning transactions with options: #{options}"
|
49
72
|
end
|
50
73
|
|
@@ -95,5 +118,7 @@ class BankReconciliationCLI
|
|
95
118
|
end
|
96
119
|
end
|
97
120
|
|
121
|
+
Appydave::Tools::Configuration::Config.configure
|
122
|
+
|
98
123
|
BankReconciliationCLI.new.run
|
99
124
|
# BankReconciliationCLI.new.run if __FILE__ == $PROGRAM_NAME
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
5
|
+
|
6
|
+
require 'appydave/tools'
|
7
|
+
|
8
|
+
# Process command line arguments for SubtitleMaster operations
|
9
|
+
class SubtitleMasterCLI
|
10
|
+
def initialize
|
11
|
+
@commands = {
|
12
|
+
'clean' => method(:clean_subtitles),
|
13
|
+
'correct' => method(:correct_subtitles),
|
14
|
+
'split' => method(:split_subtitles),
|
15
|
+
'highlight' => method(:highlight_subtitles),
|
16
|
+
'image_prompts' => method(:generate_image_prompts)
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
command, *args = ARGV
|
22
|
+
if @commands.key?(command)
|
23
|
+
@commands[command].call(args)
|
24
|
+
else
|
25
|
+
puts "Unknown command: #{command}"
|
26
|
+
print_help
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def clean_subtitles(args)
|
33
|
+
options = parse_options(args, 'clean')
|
34
|
+
cleaner = Appydave::Tools::SubtitleMaster::Clean.new(options[:file])
|
35
|
+
result = cleaner.clean
|
36
|
+
write_output(result, options[:output])
|
37
|
+
end
|
38
|
+
|
39
|
+
def correct_subtitles(args)
|
40
|
+
options = parse_options(args, 'correct')
|
41
|
+
corrector = Appydave::Tools::SubtitleMaster::Correct.new(options[:file])
|
42
|
+
result = corrector.correct
|
43
|
+
write_output(result, options[:output])
|
44
|
+
end
|
45
|
+
|
46
|
+
def split_subtitles(args)
|
47
|
+
options = parse_options(args, 'split', %i[words_per_group])
|
48
|
+
splitter = Appydave::Tools::SubtitleMaster::Split.new(options[:file], options[:words_per_group])
|
49
|
+
result = splitter.split
|
50
|
+
write_output(result, options[:output])
|
51
|
+
end
|
52
|
+
|
53
|
+
def highlight_subtitles(args)
|
54
|
+
options = parse_options(args, 'highlight')
|
55
|
+
highlighter = Appydave::Tools::SubtitleMaster::Highlight.new(options[:file])
|
56
|
+
result = highlighter.highlight
|
57
|
+
write_output(result, options[:output])
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate_image_prompts(args)
|
61
|
+
options = parse_options(args, 'image_prompts')
|
62
|
+
image_prompter = Appydave::Tools::SubtitleMaster::ImagePrompts.new(options[:file])
|
63
|
+
result = image_prompter.generate_prompts
|
64
|
+
write_output(result, options[:output])
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_options(args, command, extra_options = [])
|
68
|
+
options = { file: nil, output: nil }
|
69
|
+
OptionParser.new do |opts|
|
70
|
+
opts.banner = "Usage: subtitle_master.rb #{command} [options]"
|
71
|
+
|
72
|
+
opts.on('-f', '--file FILE', 'SRT file to process') { |v| options[:file] = v }
|
73
|
+
opts.on('-o', '--output FILE', 'Output file') { |v| options[:output] = v }
|
74
|
+
|
75
|
+
extra_options.each do |opt|
|
76
|
+
case opt
|
77
|
+
when :words_per_group
|
78
|
+
opts.on('-w', '--words-per-group WORDS', 'Number of words per group for splitting') { |v| options[:words_per_group] = v.to_i }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
83
|
+
puts opts
|
84
|
+
exit
|
85
|
+
end
|
86
|
+
end.parse!(args)
|
87
|
+
|
88
|
+
unless options[:file] && options[:output]
|
89
|
+
puts 'Missing required options. Use -h for help.'
|
90
|
+
exit
|
91
|
+
end
|
92
|
+
|
93
|
+
options
|
94
|
+
end
|
95
|
+
|
96
|
+
def write_output(result, output_file)
|
97
|
+
File.write(output_file, result)
|
98
|
+
puts "Processed file written to #{output_file}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def print_help
|
102
|
+
puts 'Usage: subtitle_master.rb [command] [options]'
|
103
|
+
puts 'Commands:'
|
104
|
+
puts ' clean Clean and normalize SRT files'
|
105
|
+
puts ' correct Correct common typos and mistranslations in SRT files'
|
106
|
+
puts ' split Split subtitle groups based on word count'
|
107
|
+
puts ' highlight Highlight power words in subtitles'
|
108
|
+
puts ' image_prompts Generate image prompts from subtitle text'
|
109
|
+
puts "Run 'subtitle_master.rb [command] --help' for more information on a command."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
SubtitleMasterCLI.new.run
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
5
|
+
|
6
|
+
require 'appydave/tools'
|
7
|
+
|
8
|
+
# Process command line arguments for YouTubeVideoManager operations
|
9
|
+
class YouTubeVideoManagerCLI
|
10
|
+
def initialize
|
11
|
+
@commands = {
|
12
|
+
'get' => method(:fetch_video_details)
|
13
|
+
# Additional commands can be added here
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
command, *args = ARGV
|
19
|
+
if @commands.key?(command)
|
20
|
+
@commands[command].call(args)
|
21
|
+
else
|
22
|
+
puts "Unknown command: #{command}"
|
23
|
+
print_help
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def fetch_video_details(args)
|
30
|
+
options = parse_options(args, 'get')
|
31
|
+
manager = Appydave::Tools::YouTubeManager::GetVideo.new
|
32
|
+
manager.get(options[:video_id])
|
33
|
+
# json = JSON.pretty_generate(details)
|
34
|
+
# puts json
|
35
|
+
|
36
|
+
# report = Appydave::Tools::YouTubeManager::Reports::VideoDetailsReport.new
|
37
|
+
# report.print(manager.data)
|
38
|
+
|
39
|
+
report = Appydave::Tools::YouTubeManager::Reports::VideoContentReport.new
|
40
|
+
report.print(manager.data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_options(args, command)
|
44
|
+
options = { video_id: nil }
|
45
|
+
OptionParser.new do |opts|
|
46
|
+
opts.banner = "Usage: youtube_video_manager.rb #{command} [options]"
|
47
|
+
|
48
|
+
opts.on('-v', '--video-id ID', 'YouTube Video ID') { |v| options[:video_id] = v }
|
49
|
+
|
50
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
51
|
+
puts opts
|
52
|
+
exit
|
53
|
+
end
|
54
|
+
end.parse!(args)
|
55
|
+
|
56
|
+
unless options[:video_id]
|
57
|
+
puts 'Missing required options. Use -h for help.'
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
|
61
|
+
options
|
62
|
+
end
|
63
|
+
|
64
|
+
def print_help
|
65
|
+
puts 'Usage: youtube_video_manager.rb [command] [options]'
|
66
|
+
puts 'Commands:'
|
67
|
+
puts ' get Get details for a YouTube video'
|
68
|
+
# Additional commands can be listed here
|
69
|
+
puts "Run 'youtube_video_manager.rb [command] --help' for more information on a command."
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
YouTubeVideoManagerCLI.new.run
|
@@ -10,24 +10,31 @@ module Appydave
|
|
10
10
|
include KLog::Logging
|
11
11
|
|
12
12
|
attr_reader :transaction_folder
|
13
|
+
attr_reader :output_folder
|
13
14
|
attr_reader :transactions
|
14
15
|
|
15
16
|
# (config_file)
|
16
|
-
def initialize(transaction_folder:
|
17
|
+
def initialize(transaction_folder: nil, output_folder: nil)
|
18
|
+
# needs to use config.bank_reconciliation.transaction_folder
|
19
|
+
transaction_folder ||= '/Volumes/Expansion/Sync/bank-reconciliation/original-transactions'
|
20
|
+
output_folder ||= File.join(transaction_folder, 'clean')
|
21
|
+
|
17
22
|
@transaction_folder = transaction_folder
|
23
|
+
@output_folder = output_folder
|
18
24
|
end
|
19
25
|
|
20
|
-
def clean_transactions(input_globs,
|
26
|
+
def clean_transactions(input_globs, output_file)
|
21
27
|
raw_transactions = grab_raw_transactions(input_globs)
|
22
28
|
transactions, duplicates_count = deduplicate(raw_transactions)
|
23
29
|
|
24
30
|
transactions = Mapper.new.map(transactions)
|
25
|
-
|
31
|
+
|
32
|
+
# tp transactions, Appydave::Tools::BankReconciliation::Models::Transaction.csv_headers
|
26
33
|
|
27
34
|
log.kv 'Deduped consolidated transactions', duplicates_count if duplicates_count.positive?
|
28
35
|
|
29
|
-
|
30
|
-
|
36
|
+
save_to_csv(transactions, output_file)
|
37
|
+
|
31
38
|
@transactions = transactions
|
32
39
|
end
|
33
40
|
|
@@ -42,6 +49,7 @@ module Appydave
|
|
42
49
|
|
43
50
|
input_globs.each do |glob|
|
44
51
|
Dir.glob(glob).each do |file|
|
52
|
+
log.kv 'Reading transactions from', file
|
45
53
|
raw_transactions = ReadTransactions.new(file).read
|
46
54
|
deduped_transactions, duplicates_count = deduplicate(raw_transactions)
|
47
55
|
|
@@ -81,8 +89,11 @@ module Appydave
|
|
81
89
|
end
|
82
90
|
|
83
91
|
def save_to_csv(transactions, output_file)
|
92
|
+
FileUtils.mkdir_p(output_folder)
|
93
|
+
output_file = File.join(output_folder, output_file)
|
94
|
+
|
84
95
|
CSV.open(output_file, 'w') do |csv|
|
85
|
-
csv <<
|
96
|
+
csv << Appydave::Tools::BankReconciliation::Models::Transaction.csv_headers
|
86
97
|
transactions.each do |transaction|
|
87
98
|
csv << transaction.to_csv_row
|
88
99
|
end
|
@@ -40,7 +40,9 @@ module Appydave
|
|
40
40
|
trigram_match(transaction, 0.7, '70%') ||
|
41
41
|
trigram_match(transaction, 0.6, '60%') ||
|
42
42
|
trigram_match(transaction, 0.5, '50%') ||
|
43
|
-
transaction
|
43
|
+
start_with_match(transaction) ||
|
44
|
+
includes(transaction)
|
45
|
+
transaction
|
44
46
|
end
|
45
47
|
|
46
48
|
def map_bank_account(transaction)
|
@@ -56,7 +58,7 @@ module Appydave
|
|
56
58
|
|
57
59
|
def equality_match(transaction)
|
58
60
|
coa = config.bank_reconciliation.chart_of_accounts.find do |chart_of_account|
|
59
|
-
chart_of_account.narration.to_s.delete(' ') == transaction.narration.delete(' ')
|
61
|
+
chart_of_account.narration.to_s.delete(' ').downcase == transaction.narration.delete(' ').downcase
|
60
62
|
end
|
61
63
|
|
62
64
|
return nil unless coa
|
@@ -66,6 +68,30 @@ module Appydave
|
|
66
68
|
transaction
|
67
69
|
end
|
68
70
|
|
71
|
+
def start_with_match(transaction)
|
72
|
+
coa = config.bank_reconciliation.chart_of_accounts.find do |chart_of_account|
|
73
|
+
transaction.narration.to_s.delete(' ').downcase.start_with?(chart_of_account.narration.to_s.downcase)
|
74
|
+
end
|
75
|
+
|
76
|
+
return nil unless coa
|
77
|
+
|
78
|
+
transaction.coa_match_type = 'starts_with'
|
79
|
+
transaction.coa_code = coa.code
|
80
|
+
transaction
|
81
|
+
end
|
82
|
+
|
83
|
+
def includes(transaction)
|
84
|
+
coa = config.bank_reconciliation.chart_of_accounts.find do |chart_of_account|
|
85
|
+
transaction.narration.to_s.delete(' ').downcase.include?(chart_of_account.narration.delete(' ').to_s.downcase)
|
86
|
+
end
|
87
|
+
|
88
|
+
return nil unless coa
|
89
|
+
|
90
|
+
transaction.coa_match_type = 'includes'
|
91
|
+
transaction.coa_code = coa.code
|
92
|
+
transaction
|
93
|
+
end
|
94
|
+
|
69
95
|
def trigram_match(transaction, score_threshold, match_type)
|
70
96
|
scored_transactions = config.bank_reconciliation.chart_of_accounts.map do |coa|
|
71
97
|
{
|
@@ -21,6 +21,8 @@ module Appydave
|
|
21
21
|
case platform
|
22
22
|
when :bankwest
|
23
23
|
read_bankwest(csv_lines)
|
24
|
+
when :bankwest2
|
25
|
+
read_bankwest2(csv_lines)
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
@@ -48,11 +50,35 @@ module Appydave
|
|
48
50
|
@transactions
|
49
51
|
end
|
50
52
|
|
53
|
+
def read_bankwest2(csv_lines)
|
54
|
+
@transactions = []
|
55
|
+
|
56
|
+
# Skip the header line and parse each subsequent line
|
57
|
+
CSV.parse(csv_lines.join, headers: true).each do |row|
|
58
|
+
transaction = Models::Transaction.new(
|
59
|
+
bsb_number: row['BSB / Account Number'].split(' - ').first,
|
60
|
+
account_number: row['BSB / Account Number'].split(' - ').last,
|
61
|
+
transaction_date: row['Transaction Date'],
|
62
|
+
narration: row['Narration'],
|
63
|
+
cheque_number: row['Cheque Number'],
|
64
|
+
debit: row['Debit'],
|
65
|
+
credit: row['Credit'],
|
66
|
+
balance: row['Balance'],
|
67
|
+
transaction_type: row['Transaction Type']
|
68
|
+
)
|
69
|
+
@transactions << transaction
|
70
|
+
end
|
71
|
+
|
72
|
+
@transactions
|
73
|
+
end
|
74
|
+
|
51
75
|
# For bankwest the first row is the CSV will look like:
|
52
76
|
# BSB Number,Account Number,Transaction Date,Narration,Cheque Number,Debit,Credit,Balance,Transaction Type
|
53
77
|
def detect_platform(csv_lines)
|
54
78
|
return :bankwest if csv_lines.first.start_with?('BSB Number,Account Number,Transaction Date,Narration,Cheque Number,Debit,Credit,Balance,Transaction Type')
|
79
|
+
return :bankwest2 if csv_lines.first.start_with?('Account Name,BSB / Account Number,Transaction Date,Narration,Cheque Number,Debit,Credit,Balance,Transaction Type')
|
55
80
|
|
81
|
+
puts "Unknown platform detected. CSV columns are: #{csv_lines.first.strip}"
|
56
82
|
raise Appydave::Tools::Error, 'Unknown platform'
|
57
83
|
end
|
58
84
|
end
|
@@ -48,6 +48,42 @@ module Appydave
|
|
48
48
|
@coa_match_type = coa_match_type
|
49
49
|
@account_name = account_name
|
50
50
|
end
|
51
|
+
|
52
|
+
def self.csv_headers
|
53
|
+
%i[
|
54
|
+
bsb_number
|
55
|
+
account_number
|
56
|
+
transaction_date
|
57
|
+
narration
|
58
|
+
cheque_number
|
59
|
+
debit
|
60
|
+
credit
|
61
|
+
balance
|
62
|
+
transaction_type
|
63
|
+
platform
|
64
|
+
coa_code
|
65
|
+
coa_match_type
|
66
|
+
account_name
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_csv_row
|
71
|
+
[
|
72
|
+
@bsb_number,
|
73
|
+
@account_number,
|
74
|
+
@transaction_date,
|
75
|
+
@narration,
|
76
|
+
@cheque_number,
|
77
|
+
@debit,
|
78
|
+
@credit,
|
79
|
+
@balance,
|
80
|
+
@transaction_type,
|
81
|
+
@platform,
|
82
|
+
@coa_code,
|
83
|
+
@coa_match_type,
|
84
|
+
@account_name
|
85
|
+
]
|
86
|
+
end
|
51
87
|
end
|
52
88
|
end
|
53
89
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
# Hash with indifferent access
|
6
|
+
class IndifferentAccessHash < Hash
|
7
|
+
def initialize(initial_hash = {})
|
8
|
+
super()
|
9
|
+
update(initial_hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
super(convert_key(key))
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, value)
|
17
|
+
super(convert_key(key), value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch(key, *args)
|
21
|
+
super(convert_key(key), *args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(key)
|
25
|
+
super(convert_key(key))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def convert_key(key)
|
31
|
+
key.is_a?(Symbol) ? key.to_s : key
|
32
|
+
end
|
33
|
+
|
34
|
+
def update(initial_hash)
|
35
|
+
initial_hash.each do |key, value|
|
36
|
+
self[convert_key(key)] = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Subtitle Master
|
2
|
+
|
3
|
+
[ChatGPT](https://chatgpt.com/c/f80dfca5-8168-4561-b5c6-8efed8672a88)
|
4
|
+
|
5
|
+
## SubtitleMaster - Clean Component
|
6
|
+
|
7
|
+
The `SubtitleMaster::Clean` component is designed to process subtitle (SRT) files to improve their readability and compatibility with various platforms like YouTube. The main functionalities of this component include:
|
8
|
+
|
9
|
+
- **Removing HTML Tags**: Strips out unnecessary HTML underline tags (`<u>` and `</u>`) that may be present in the subtitle content.
|
10
|
+
- **Normalizing Subtitle Lines**: Merges fragmented subtitle lines into coherent sentences. It adjusts the timestamps to ensure that each subtitle entry spans the correct duration, combining lines that were incorrectly split by the subtitle creation software.
|
11
|
+
- **Handling Timestamps**: Updates the end timestamp of each merged subtitle to reflect the actual end time of the last occurrence of the text, ensuring accurate timing for each subtitle entry.
|
12
|
+
|
13
|
+
This component reads the SRT file, processes the content to remove tags and normalize lines, and outputs a cleaned and formatted subtitle file that is easier to read and upload to platforms.
|
14
|
+
|
15
|
+
```bash
|
16
|
+
./bin/subtitle_master.rb clean -f path/to/example.srt -o path/to/example_cleaned.srt
|
17
|
+
|
18
|
+
# Example using alias
|
19
|
+
|
20
|
+
ad_subtitle_master clean -f transcript/a45-banned-from-midjourney-16-alternatives.srt -o a45-transcript.srt
|
21
|
+
```
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module SubtitleMaster
|
6
|
+
# Clean and normalize subtitles
|
7
|
+
class Clean
|
8
|
+
def initialize(file_path)
|
9
|
+
@file_path = file_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def clean
|
13
|
+
content = File.read(@file_path, encoding: 'UTF-8')
|
14
|
+
content = remove_underscores(content)
|
15
|
+
normalize_lines(content)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def remove_underscores(content)
|
21
|
+
content.gsub(%r{</?u>}, '')
|
22
|
+
end
|
23
|
+
|
24
|
+
def normalize_lines(content)
|
25
|
+
lines = content.split("\n")
|
26
|
+
grouped_subtitles = []
|
27
|
+
current_subtitle = { text: '', start_time: nil, end_time: nil }
|
28
|
+
|
29
|
+
lines.each do |line|
|
30
|
+
if line =~ /^\d+$/ || line.strip.empty?
|
31
|
+
next
|
32
|
+
elsif line =~ /^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$/
|
33
|
+
if current_subtitle[:start_time]
|
34
|
+
grouped_subtitles << current_subtitle.clone
|
35
|
+
current_subtitle = { text: '', start_time: nil, end_time: nil }
|
36
|
+
end
|
37
|
+
|
38
|
+
times = line.split(' --> ')
|
39
|
+
current_subtitle[:start_time] = times[0]
|
40
|
+
current_subtitle[:end_time] = times[1]
|
41
|
+
else
|
42
|
+
current_subtitle[:text] += ' ' unless current_subtitle[:text].empty?
|
43
|
+
current_subtitle[:text] += line.strip
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
grouped_subtitles << current_subtitle unless current_subtitle[:text].empty?
|
48
|
+
|
49
|
+
grouped_subtitles = merge_subtitles(grouped_subtitles)
|
50
|
+
|
51
|
+
build_normalized_content(grouped_subtitles)
|
52
|
+
end
|
53
|
+
|
54
|
+
def merge_subtitles(subtitles)
|
55
|
+
merged_subtitles = []
|
56
|
+
subtitles.each do |subtitle|
|
57
|
+
if merged_subtitles.empty? || merged_subtitles.last[:text] != subtitle[:text]
|
58
|
+
merged_subtitles << subtitle
|
59
|
+
else
|
60
|
+
merged_subtitles.last[:end_time] = subtitle[:end_time]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
merged_subtitles
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_normalized_content(grouped_subtitles)
|
67
|
+
normalized_content = []
|
68
|
+
grouped_subtitles.each_with_index do |subtitle, index|
|
69
|
+
normalized_content << (index + 1).to_s
|
70
|
+
normalized_content << "#{subtitle[:start_time]} --> #{subtitle[:end_time]}"
|
71
|
+
normalized_content << subtitle[:text]
|
72
|
+
normalized_content << ''
|
73
|
+
end
|
74
|
+
|
75
|
+
normalized_content.join("\n")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module YouTubeManager
|
6
|
+
# Handle YouTube API Authorization
|
7
|
+
class Authorization
|
8
|
+
REDIRECT_URI = 'http://localhost:8080/'
|
9
|
+
CLIENT_SECRETS_PATH = File.join(Dir.home, '.config', 'appydave-google-youtube.json')
|
10
|
+
CREDENTIALS_PATH = File.join(Dir.home, '.credentials', 'ad_youtube.yaml')
|
11
|
+
|
12
|
+
SCOPE = [
|
13
|
+
'https://www.googleapis.com/auth/youtube.readonly',
|
14
|
+
'https://www.googleapis.com/auth/youtube'
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
def self.authorize
|
18
|
+
FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))
|
19
|
+
|
20
|
+
client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
|
21
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
|
22
|
+
authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
|
23
|
+
user_id = 'default'
|
24
|
+
credentials = authorizer.get_credentials(user_id)
|
25
|
+
credentials = wait_for_authorization(authorizer) if credentials.nil?
|
26
|
+
credentials
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.wait_for_authorization(authorizer)
|
30
|
+
url = authorizer.get_authorization_url(base_url: REDIRECT_URI)
|
31
|
+
puts 'Open the following URL in your browser and authorize the application:'
|
32
|
+
puts url
|
33
|
+
|
34
|
+
server = WEBrick::HTTPServer.new(Port: 8080, AccessLog: [], Logger: WEBrick::Log.new(nil, 0))
|
35
|
+
trap('INT') { server.shutdown }
|
36
|
+
|
37
|
+
server.mount_proc '/' do |req, res|
|
38
|
+
auth_code = req.query['code']
|
39
|
+
res.body = 'Authorization successful. You can close this window now.'
|
40
|
+
server.shutdown
|
41
|
+
authorizer.get_and_store_credentials_from_code(
|
42
|
+
user_id: user_id, code: auth_code, base_url: REDIRECT_URI
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
server.start
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module YouTubeManager
|
6
|
+
# Manage YouTube video details
|
7
|
+
class GetVideo < YouTubeBase
|
8
|
+
attr_reader :video_id
|
9
|
+
attr_reader :data
|
10
|
+
attr_reader :video
|
11
|
+
|
12
|
+
def get(video_id)
|
13
|
+
@video_id = video_id
|
14
|
+
response = @service.list_videos('snippet,contentDetails,status,statistics', id: video_id)
|
15
|
+
@video = response.items.first
|
16
|
+
|
17
|
+
data = {
|
18
|
+
id: video.id,
|
19
|
+
title: video.snippet.title,
|
20
|
+
description: video.snippet.description,
|
21
|
+
published_at: video.snippet.published_at,
|
22
|
+
channel_id: video.snippet.channel_id,
|
23
|
+
channel_title: video.snippet.channel_title,
|
24
|
+
view_count: video.statistics.view_count,
|
25
|
+
like_count: video.statistics.like_count,
|
26
|
+
dislike_count: video.statistics.dislike_count,
|
27
|
+
comment_count: video.statistics.comment_count,
|
28
|
+
privacy_status: video.status.privacy_status,
|
29
|
+
embeddable: video.status.embeddable,
|
30
|
+
license: video.status.license,
|
31
|
+
recording_location: video.recording_details&.location,
|
32
|
+
recording_date: video.recording_details&.recording_date,
|
33
|
+
tags: video.snippet.tags,
|
34
|
+
thumbnails: video.snippet.thumbnails.to_h,
|
35
|
+
duration: video.content_details.duration,
|
36
|
+
definition: video.content_details.definition,
|
37
|
+
caption: video.content_details.caption,
|
38
|
+
licensed_content: video.content_details.licensed_content
|
39
|
+
}
|
40
|
+
|
41
|
+
@data = Appydave::Tools::IndifferentAccessHash.new(data)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module YouTubeManager
|
6
|
+
module Reports
|
7
|
+
# Report on video content
|
8
|
+
class VideoContentReport
|
9
|
+
include KLog::Logging
|
10
|
+
|
11
|
+
def print(data)
|
12
|
+
# log.heading 'Video Details Report'
|
13
|
+
log.subheading data[:title]
|
14
|
+
log.kv 'Published At', data[:published_at]
|
15
|
+
log.kv 'View Count', data[:view_count]
|
16
|
+
log.kv 'Like Count', data[:like_count]
|
17
|
+
log.kv 'Dislike Count', data[:dislike_count]
|
18
|
+
log.kv 'Tags', data[:tags].join(', ')
|
19
|
+
log.line
|
20
|
+
puts data[:description]
|
21
|
+
log.line
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module YouTubeManager
|
6
|
+
module Reports
|
7
|
+
# Print video details
|
8
|
+
class VideoDetailsReport
|
9
|
+
include KLog::Logging
|
10
|
+
|
11
|
+
def print(data)
|
12
|
+
# log.heading 'Video Details Report'
|
13
|
+
# log.subheading 'Video Details Report'
|
14
|
+
log.section_heading 'Video Details Report'
|
15
|
+
log.kv 'ID', data[:id]
|
16
|
+
log.kv 'Title', data[:title]
|
17
|
+
log.kv 'Published At', data[:published_at]
|
18
|
+
log.kv 'View Count', data[:view_count]
|
19
|
+
log.kv 'Like Count', data[:like_count]
|
20
|
+
log.kv 'Dislike Count', data[:dislike_count]
|
21
|
+
log.kv 'Comment Count', data[:comment_count]
|
22
|
+
log.kv 'Privacy Status', data[:privacy_status]
|
23
|
+
log.kv 'Channel ID', data[:channel_id]
|
24
|
+
log.kv 'Channel Title', data[:channel_title]
|
25
|
+
log.kv 'Embeddable', data[:embeddable]
|
26
|
+
log.kv 'License', data[:license]
|
27
|
+
log.kv 'Recording Location', data[:recording_location]
|
28
|
+
log.kv 'Recording Date', data[:recording_date]
|
29
|
+
log.kv 'Tags', data[:tags].join(', ')
|
30
|
+
log.kv data[:description][0..100]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module YouTubeManager
|
6
|
+
# Base class for YouTube API management
|
7
|
+
class YouTubeBase
|
8
|
+
def initialize
|
9
|
+
@service = Google::Apis::YoutubeV3::YouTubeService.new
|
10
|
+
@service.client_options.application_name = 'YouTube Video Manager'
|
11
|
+
@service.authorization = Authorization.authorize
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/appydave/tools.rb
CHANGED
@@ -9,7 +9,16 @@ require 'openai'
|
|
9
9
|
require 'optparse'
|
10
10
|
require 'k_log'
|
11
11
|
|
12
|
+
require 'google/apis/youtube_v3'
|
13
|
+
require 'googleauth'
|
14
|
+
require 'googleauth/stores/file_token_store'
|
15
|
+
require 'webrick'
|
16
|
+
|
17
|
+
require 'pry'
|
18
|
+
|
12
19
|
require 'appydave/tools/version'
|
20
|
+
require 'appydave/tools/indifferent_access_hash'
|
21
|
+
|
13
22
|
require 'appydave/tools/gpt_context/file_collector'
|
14
23
|
|
15
24
|
require 'appydave/tools/configuration/openai'
|
@@ -25,6 +34,14 @@ require 'appydave/tools/bank_reconciliation/clean/read_transactions'
|
|
25
34
|
require 'appydave/tools/bank_reconciliation/clean/mapper'
|
26
35
|
require 'appydave/tools/bank_reconciliation/models/transaction'
|
27
36
|
|
37
|
+
require 'appydave/tools/subtitle_master/clean'
|
38
|
+
|
39
|
+
require 'appydave/tools/youtube_manager/youtube_base'
|
40
|
+
require 'appydave/tools/youtube_manager/authorization'
|
41
|
+
require 'appydave/tools/youtube_manager/get_video'
|
42
|
+
require 'appydave/tools/youtube_manager/reports/video_details_report'
|
43
|
+
require 'appydave/tools/youtube_manager/reports/video_content_report'
|
44
|
+
|
28
45
|
Appydave::Tools::Configuration::Config.set_default do |config|
|
29
46
|
config.config_path = File.expand_path('~/.config/appydave')
|
30
47
|
config.register(:settings, Appydave::Tools::Configuration::Models::SettingsConfig)
|
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "appydave-tools",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.9.0",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "appydave-tools",
|
9
|
-
"version": "0.
|
9
|
+
"version": "0.9.0",
|
10
10
|
"devDependencies": {
|
11
11
|
"@klueless-js/semantic-release-rubygem": "github:klueless-js/semantic-release-rubygem",
|
12
12
|
"@semantic-release/changelog": "^6.0.3",
|
data/package.json
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: appydave-tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Cruwys
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: clipboard
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: google-api-client
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.53'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.53'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: k_log
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,34 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '7'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: googleauth
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: webrick
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
83
125
|
description: " AppyDave YouTube Automation Tools\n"
|
84
126
|
email:
|
85
127
|
- david@ideasmen.com.au
|
@@ -105,6 +147,8 @@ files:
|
|
105
147
|
- bin/console
|
106
148
|
- bin/gpt_context.rb
|
107
149
|
- bin/setup
|
150
|
+
- bin/subtitle_master.rb
|
151
|
+
- bin/youtube_manager.rb
|
108
152
|
- images.log
|
109
153
|
- lib/appydave/tools.rb
|
110
154
|
- lib/appydave/tools/bank_reconciliation/_doc.md
|
@@ -123,9 +167,17 @@ files:
|
|
123
167
|
- lib/appydave/tools/configuration/openai.rb
|
124
168
|
- lib/appydave/tools/gpt_context/_doc.md
|
125
169
|
- lib/appydave/tools/gpt_context/file_collector.rb
|
170
|
+
- lib/appydave/tools/indifferent_access_hash.rb
|
126
171
|
- lib/appydave/tools/name_manager/_doc.md
|
127
172
|
- lib/appydave/tools/name_manager/project_name.rb
|
173
|
+
- lib/appydave/tools/subtitle_master/_doc.md
|
174
|
+
- lib/appydave/tools/subtitle_master/clean.rb
|
128
175
|
- lib/appydave/tools/version.rb
|
176
|
+
- lib/appydave/tools/youtube_manager/authorization.rb
|
177
|
+
- lib/appydave/tools/youtube_manager/get_video.rb
|
178
|
+
- lib/appydave/tools/youtube_manager/reports/video_content_report.rb
|
179
|
+
- lib/appydave/tools/youtube_manager/reports/video_details_report.rb
|
180
|
+
- lib/appydave/tools/youtube_manager/youtube_base.rb
|
129
181
|
- package-lock.json
|
130
182
|
- package.json
|
131
183
|
- sig/appydave/tools.rbs
|