appydave-tools 0.17.1 → 0.18.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/CLAUDE.md +44 -7
- data/README.md +8 -5
- data/bin/{vat → dam} +163 -83
- data/docs/{vat → dam}/dam-vision.md +13 -13
- data/docs/{vat → dam}/session-summary-2025-11-09.md +79 -79
- data/docs/{vat → dam}/usage.md +118 -93
- data/docs/{vat → dam}/vat-testing-plan.md +94 -94
- data/docs/development/CODEX-recommendations.md +11 -0
- data/lib/appydave/tools/configuration/models/brands_config.rb +18 -3
- data/lib/appydave/tools/{vat → dam}/config.rb +32 -13
- data/lib/appydave/tools/{vat → dam}/config_loader.rb +1 -1
- data/lib/appydave/tools/{vat → dam}/manifest_generator.rb +3 -3
- data/lib/appydave/tools/{vat → dam}/project_listing.rb +1 -1
- data/lib/appydave/tools/{vat → dam}/project_resolver.rb +1 -1
- data/lib/appydave/tools/{vat → dam}/s3_operations.rb +3 -3
- data/lib/appydave/tools/dam/sync_from_ssd.rb +241 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +7 -6
- data/package.json +1 -1
- metadata +13 -12
|
@@ -7,18 +7,18 @@ require 'aws-sdk-s3'
|
|
|
7
7
|
|
|
8
8
|
module Appydave
|
|
9
9
|
module Tools
|
|
10
|
-
module
|
|
10
|
+
module Dam
|
|
11
11
|
# S3 operations for VAT (upload, download, status, cleanup)
|
|
12
12
|
class S3Operations
|
|
13
13
|
attr_reader :brand_info, :brand, :project_id, :brand_path, :s3_client
|
|
14
14
|
|
|
15
15
|
def initialize(brand, project_id, brand_info: nil, brand_path: nil, s3_client: nil)
|
|
16
|
-
@brand = brand
|
|
17
16
|
@project_id = project_id
|
|
18
17
|
|
|
19
18
|
# Use injected dependencies or load from configuration
|
|
20
|
-
@brand_path = brand_path || Config.brand_path(brand)
|
|
21
19
|
@brand_info = brand_info || load_brand_info(brand)
|
|
20
|
+
@brand = @brand_info.key # Use resolved brand key, not original input
|
|
21
|
+
@brand_path = brand_path || Config.brand_path(@brand)
|
|
22
22
|
@s3_client = s3_client || create_s3_client(@brand_info)
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Appydave
|
|
7
|
+
module Tools
|
|
8
|
+
module Dam
|
|
9
|
+
# Sync light files from SSD to local for a brand
|
|
10
|
+
# Only copies non-video files (subtitles, images, docs)
|
|
11
|
+
class SyncFromSsd
|
|
12
|
+
attr_reader :brand, :brand_info, :brand_path
|
|
13
|
+
|
|
14
|
+
# Light file patterns to include (everything except heavy video files)
|
|
15
|
+
LIGHT_FILE_PATTERNS = %w[
|
|
16
|
+
**/*.srt
|
|
17
|
+
**/*.vtt
|
|
18
|
+
**/*.txt
|
|
19
|
+
**/*.md
|
|
20
|
+
**/*.jpg
|
|
21
|
+
**/*.jpeg
|
|
22
|
+
**/*.png
|
|
23
|
+
**/*.webp
|
|
24
|
+
**/*.json
|
|
25
|
+
**/*.yml
|
|
26
|
+
**/*.yaml
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
# Heavy file patterns to exclude (video files)
|
|
30
|
+
HEAVY_FILE_PATTERNS = %w[
|
|
31
|
+
*.mp4
|
|
32
|
+
*.mov
|
|
33
|
+
*.avi
|
|
34
|
+
*.mkv
|
|
35
|
+
*.webm
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(brand, brand_info: nil, brand_path: nil)
|
|
39
|
+
@brand_info = brand_info || load_brand_info(brand)
|
|
40
|
+
@brand = @brand_info.key # Use resolved brand key, not original input
|
|
41
|
+
@brand_path = brand_path || Config.brand_path(@brand)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Sync light files from SSD for all projects in manifest
|
|
45
|
+
def sync(dry_run: false)
|
|
46
|
+
puts dry_run ? '🔍 DRY-RUN MODE - No files will be copied' : '📦 Syncing from SSD...'
|
|
47
|
+
puts ''
|
|
48
|
+
|
|
49
|
+
# Validate SSD is mounted
|
|
50
|
+
ssd_backup = brand_info.locations.ssd_backup
|
|
51
|
+
unless ssd_backup && !ssd_backup.empty?
|
|
52
|
+
puts "❌ SSD backup location not configured for brand '#{brand}'"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless Dir.exist?(ssd_backup)
|
|
57
|
+
puts "❌ SSD not mounted at #{ssd_backup}"
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Load manifest
|
|
62
|
+
manifest_file = File.join(brand_path, 'projects.json')
|
|
63
|
+
unless File.exist?(manifest_file)
|
|
64
|
+
puts '❌ projects.json not found!'
|
|
65
|
+
puts " Run: vat manifest #{brand}"
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
manifest = load_manifest(manifest_file)
|
|
70
|
+
puts "📋 Loaded manifest: #{manifest[:projects].size} projects"
|
|
71
|
+
puts " Last updated: #{manifest[:config][:last_updated]}"
|
|
72
|
+
puts ''
|
|
73
|
+
|
|
74
|
+
# Filter projects to sync
|
|
75
|
+
projects_to_sync = manifest[:projects].select { |p| should_sync_project?(p) }
|
|
76
|
+
|
|
77
|
+
puts '🔍 Analysis:'
|
|
78
|
+
puts " Total projects in manifest: #{manifest[:projects].size}"
|
|
79
|
+
puts " Projects to sync: #{projects_to_sync.size}"
|
|
80
|
+
puts " Skipped (already local): #{manifest[:projects].size - projects_to_sync.size}"
|
|
81
|
+
puts ''
|
|
82
|
+
|
|
83
|
+
if projects_to_sync.empty?
|
|
84
|
+
puts '✅ Nothing to sync - all projects either already local or not on SSD'
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Sync each project
|
|
89
|
+
total_stats = { files: 0, bytes: 0, skipped: 0 }
|
|
90
|
+
|
|
91
|
+
projects_to_sync.each do |project|
|
|
92
|
+
stats = sync_project(project, ssd_backup, dry_run: dry_run)
|
|
93
|
+
|
|
94
|
+
# Only show project if there are files to sync or a warning
|
|
95
|
+
if stats[:reason] || stats[:files]&.positive?
|
|
96
|
+
puts "📁 #{project[:id]}"
|
|
97
|
+
|
|
98
|
+
puts " ⚠️ Skipped: #{stats[:reason]}" if stats[:reason]
|
|
99
|
+
|
|
100
|
+
puts " #{stats[:files]} file(s), #{format_bytes(stats[:bytes])}" if stats[:files]&.positive?
|
|
101
|
+
puts ''
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
total_stats[:files] += stats[:files] || 0
|
|
105
|
+
total_stats[:bytes] += stats[:bytes] || 0
|
|
106
|
+
total_stats[:skipped] += stats[:skipped] || 0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
puts ''
|
|
110
|
+
puts '=' * 60
|
|
111
|
+
puts 'Summary:'
|
|
112
|
+
puts " Projects scanned: #{projects_to_sync.size}"
|
|
113
|
+
puts " Files #{dry_run ? 'to copy' : 'copied'}: #{total_stats[:files]}"
|
|
114
|
+
puts " Total size: #{format_bytes(total_stats[:bytes])}"
|
|
115
|
+
puts ''
|
|
116
|
+
puts '✅ Sync complete!'
|
|
117
|
+
puts ' Run without --dry-run to perform the sync' if dry_run
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def load_brand_info(brand)
|
|
123
|
+
Appydave::Tools::Configuration::Config.configure
|
|
124
|
+
Appydave::Tools::Configuration::Config.brands.get_brand(brand)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def load_manifest(manifest_file)
|
|
128
|
+
JSON.parse(File.read(manifest_file), symbolize_names: true)
|
|
129
|
+
rescue JSON::ParserError => e
|
|
130
|
+
puts "❌ Error parsing projects.json: #{e.message}"
|
|
131
|
+
exit 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Determine if project should be synced
|
|
135
|
+
def should_sync_project?(project)
|
|
136
|
+
# Only sync if project exists on SSD but NOT in local flat structure
|
|
137
|
+
return false unless project[:storage][:ssd][:exists]
|
|
138
|
+
|
|
139
|
+
# Skip if exists locally in flat structure
|
|
140
|
+
return false if project[:storage][:local][:exists] && project[:storage][:local][:structure] == 'flat'
|
|
141
|
+
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Sync a single project from SSD to local
|
|
146
|
+
def sync_project(project, ssd_backup, dry_run: false)
|
|
147
|
+
project_id = project[:id]
|
|
148
|
+
ssd_path = File.join(ssd_backup, project_id)
|
|
149
|
+
|
|
150
|
+
return { skipped: 1, files: 0, bytes: 0, reason: 'SSD path not found' } unless Dir.exist?(ssd_path)
|
|
151
|
+
|
|
152
|
+
# Check for flat folder conflict (stale manifest)
|
|
153
|
+
flat_path = File.join(brand_path, project_id)
|
|
154
|
+
return { skipped: 1, files: 0, bytes: 0, reason: 'Flat folder exists (stale manifest?)' } if Dir.exist?(flat_path)
|
|
155
|
+
|
|
156
|
+
# Determine local destination path (archived structure)
|
|
157
|
+
# Extract range from project ID (e.g., b65 → 60-69 range)
|
|
158
|
+
range = determine_range(project_id)
|
|
159
|
+
local_dir = File.join(brand_path, 'archived', range, project_id)
|
|
160
|
+
|
|
161
|
+
# Create local directory
|
|
162
|
+
FileUtils.mkdir_p(local_dir) if !dry_run && !Dir.exist?(local_dir)
|
|
163
|
+
|
|
164
|
+
# Sync light files
|
|
165
|
+
sync_light_files(ssd_path, local_dir, dry_run: dry_run)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Determine range folder for project (e.g., b65 → 60-69)
|
|
169
|
+
def determine_range(project_id)
|
|
170
|
+
# FliVideo pattern: b40, b41, ... b99
|
|
171
|
+
if project_id =~ /^b(\d+)/
|
|
172
|
+
tens = (Regexp.last_match(1).to_i / 10) * 10
|
|
173
|
+
"#{tens}-#{tens + 9}"
|
|
174
|
+
else
|
|
175
|
+
# Legacy pattern or unknown: use first 3 chars
|
|
176
|
+
'000-099'
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Sync light files from SSD to local
|
|
181
|
+
def sync_light_files(ssd_path, local_dir, dry_run: false)
|
|
182
|
+
stats = { files: 0, bytes: 0 }
|
|
183
|
+
|
|
184
|
+
LIGHT_FILE_PATTERNS.each do |pattern|
|
|
185
|
+
Dir.glob(File.join(ssd_path, pattern)).each do |source_file|
|
|
186
|
+
next if heavy_file?(source_file)
|
|
187
|
+
|
|
188
|
+
copy_stats = copy_light_file(source_file, ssd_path, local_dir, dry_run: dry_run)
|
|
189
|
+
stats[:files] += copy_stats[:files]
|
|
190
|
+
stats[:bytes] += copy_stats[:bytes]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
stats
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Check if file is a heavy video file
|
|
198
|
+
def heavy_file?(source_file)
|
|
199
|
+
HEAVY_FILE_PATTERNS.any? { |pattern| File.fnmatch(pattern, File.basename(source_file)) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Copy a single light file
|
|
203
|
+
def copy_light_file(source_file, ssd_path, local_dir, dry_run: false)
|
|
204
|
+
relative_path = source_file.sub("#{ssd_path}/", '')
|
|
205
|
+
dest_file = File.join(local_dir, relative_path)
|
|
206
|
+
|
|
207
|
+
# Skip if already synced (same size)
|
|
208
|
+
return { files: 0, bytes: 0 } if file_already_synced?(source_file, dest_file)
|
|
209
|
+
|
|
210
|
+
file_size = File.size(source_file)
|
|
211
|
+
|
|
212
|
+
if dry_run
|
|
213
|
+
puts " [DRY-RUN] Would copy: #{relative_path} (#{format_bytes(file_size)})"
|
|
214
|
+
else
|
|
215
|
+
FileUtils.mkdir_p(File.dirname(dest_file))
|
|
216
|
+
FileUtils.cp(source_file, dest_file, preserve: true)
|
|
217
|
+
puts " ✓ Copied: #{relative_path} (#{format_bytes(file_size)})"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
{ files: 1, bytes: file_size }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Check if file is already synced (by size comparison)
|
|
224
|
+
def file_already_synced?(source_file, dest_file)
|
|
225
|
+
File.exist?(dest_file) && File.size(dest_file) == File.size(source_file)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Format bytes into human-readable format
|
|
229
|
+
def format_bytes(bytes)
|
|
230
|
+
if bytes < 1024
|
|
231
|
+
"#{bytes}B"
|
|
232
|
+
elsif bytes < 1024 * 1024
|
|
233
|
+
"#{(bytes / 1024.0).round(1)}KB"
|
|
234
|
+
else
|
|
235
|
+
"#{(bytes / 1024.0 / 1024.0).round(1)}MB"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
data/lib/appydave/tools.rb
CHANGED
|
@@ -52,12 +52,13 @@ require 'appydave/tools/prompt_tools/prompt_completion'
|
|
|
52
52
|
require 'appydave/tools/subtitle_processor/clean'
|
|
53
53
|
require 'appydave/tools/subtitle_processor/join'
|
|
54
54
|
|
|
55
|
-
require 'appydave/tools/
|
|
56
|
-
require 'appydave/tools/
|
|
57
|
-
require 'appydave/tools/
|
|
58
|
-
require 'appydave/tools/
|
|
59
|
-
require 'appydave/tools/
|
|
60
|
-
require 'appydave/tools/
|
|
55
|
+
require 'appydave/tools/dam/config'
|
|
56
|
+
require 'appydave/tools/dam/project_resolver'
|
|
57
|
+
require 'appydave/tools/dam/config_loader'
|
|
58
|
+
require 'appydave/tools/dam/s3_operations'
|
|
59
|
+
require 'appydave/tools/dam/project_listing'
|
|
60
|
+
require 'appydave/tools/dam/manifest_generator'
|
|
61
|
+
require 'appydave/tools/dam/sync_from_ssd'
|
|
61
62
|
|
|
62
63
|
require 'appydave/tools/youtube_automation/gpt_agent'
|
|
63
64
|
|
data/package.json
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: appydave-tools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cruwys
|
|
@@ -214,6 +214,7 @@ files:
|
|
|
214
214
|
- bin/bank_reconciliation.rb
|
|
215
215
|
- bin/configuration.rb
|
|
216
216
|
- bin/console
|
|
217
|
+
- bin/dam
|
|
217
218
|
- bin/generate_manifest.rb
|
|
218
219
|
- bin/gpt_context.rb
|
|
219
220
|
- bin/move_images.rb
|
|
@@ -223,7 +224,6 @@ files:
|
|
|
223
224
|
- bin/subtitle_manager.rb
|
|
224
225
|
- bin/subtitle_processor.rb
|
|
225
226
|
- bin/sync_from_ssd.rb
|
|
226
|
-
- bin/vat
|
|
227
227
|
- bin/youtube_automation.rb
|
|
228
228
|
- bin/youtube_manager.rb
|
|
229
229
|
- docs/README.md
|
|
@@ -237,6 +237,10 @@ files:
|
|
|
237
237
|
- docs/configuration/README.md
|
|
238
238
|
- docs/configuration/channels.example.json
|
|
239
239
|
- docs/configuration/settings.example.json
|
|
240
|
+
- docs/dam/dam-vision.md
|
|
241
|
+
- docs/dam/session-summary-2025-11-09.md
|
|
242
|
+
- docs/dam/usage.md
|
|
243
|
+
- docs/dam/vat-testing-plan.md
|
|
240
244
|
- docs/development/CODEX-recommendations.md
|
|
241
245
|
- docs/development/README.md
|
|
242
246
|
- docs/development/cli-architecture-patterns.md
|
|
@@ -254,10 +258,6 @@ files:
|
|
|
254
258
|
- docs/tools/subtitle-processor.md
|
|
255
259
|
- docs/tools/youtube-automation.md
|
|
256
260
|
- docs/tools/youtube-manager.md
|
|
257
|
-
- docs/vat/dam-vision.md
|
|
258
|
-
- docs/vat/session-summary-2025-11-09.md
|
|
259
|
-
- docs/vat/usage.md
|
|
260
|
-
- docs/vat/vat-testing-plan.md
|
|
261
261
|
- exe/ad_config
|
|
262
262
|
- exe/gpt_context
|
|
263
263
|
- exe/prompt_tools
|
|
@@ -281,6 +281,13 @@ files:
|
|
|
281
281
|
- lib/appydave/tools/configuration/models/settings_config.rb
|
|
282
282
|
- lib/appydave/tools/configuration/models/youtube_automation_config.rb
|
|
283
283
|
- lib/appydave/tools/configuration/openai.rb
|
|
284
|
+
- lib/appydave/tools/dam/config.rb
|
|
285
|
+
- lib/appydave/tools/dam/config_loader.rb
|
|
286
|
+
- lib/appydave/tools/dam/manifest_generator.rb
|
|
287
|
+
- lib/appydave/tools/dam/project_listing.rb
|
|
288
|
+
- lib/appydave/tools/dam/project_resolver.rb
|
|
289
|
+
- lib/appydave/tools/dam/s3_operations.rb
|
|
290
|
+
- lib/appydave/tools/dam/sync_from_ssd.rb
|
|
284
291
|
- lib/appydave/tools/debuggable.rb
|
|
285
292
|
- lib/appydave/tools/gpt_context/_doc.md
|
|
286
293
|
- lib/appydave/tools/gpt_context/file_collector.rb
|
|
@@ -301,12 +308,6 @@ files:
|
|
|
301
308
|
- lib/appydave/tools/types/base_model.rb
|
|
302
309
|
- lib/appydave/tools/types/hash_type.rb
|
|
303
310
|
- lib/appydave/tools/types/indifferent_access_hash.rb
|
|
304
|
-
- lib/appydave/tools/vat/config.rb
|
|
305
|
-
- lib/appydave/tools/vat/config_loader.rb
|
|
306
|
-
- lib/appydave/tools/vat/manifest_generator.rb
|
|
307
|
-
- lib/appydave/tools/vat/project_listing.rb
|
|
308
|
-
- lib/appydave/tools/vat/project_resolver.rb
|
|
309
|
-
- lib/appydave/tools/vat/s3_operations.rb
|
|
310
311
|
- lib/appydave/tools/version.rb
|
|
311
312
|
- lib/appydave/tools/youtube_automation/_doc.md
|
|
312
313
|
- lib/appydave/tools/youtube_automation/gpt_agent.rb
|