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.
@@ -7,18 +7,18 @@ require 'aws-sdk-s3'
7
7
 
8
8
  module Appydave
9
9
  module Tools
10
- module Vat
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.17.1'
5
+ VERSION = '0.18.0'
6
6
  end
7
7
  end
@@ -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/vat/config'
56
- require 'appydave/tools/vat/project_resolver'
57
- require 'appydave/tools/vat/config_loader'
58
- require 'appydave/tools/vat/s3_operations'
59
- require 'appydave/tools/vat/project_listing'
60
- require 'appydave/tools/vat/manifest_generator'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
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.17.1
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