appcask 0.5.0 → 0.6.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/.idea/workspace.xml +22 -11
- data/CHANGELOG.md +24 -0
- data/README.md +3 -3
- data/examples/batch_download.rb +15 -13
- data/exe/appcask +2 -1
- data/lib/appcask/local_app.rb +139 -0
- data/lib/appcask/version.rb +1 -1
- data/lib/appcask.rb +240 -229
- metadata +48 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5e40008e983710a2983ba8a2d4005b2105319ef44f95b9509159ee348e1557c
|
|
4
|
+
data.tar.gz: 299b0bdf02be893883883ea60020bc766582e3d697c512aa2d05df3d98b26500
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bca199b0f6436e2754151b5ffaa75dccebaab9e294fc3f0e4b7b3c4dde0c70abf6a84c65580f13db2a2516232310614ed336f018695107cfbc5564f2d31055cc
|
|
7
|
+
data.tar.gz: 26b451d84723e83ec2b7afa815b14571396818bc3abd70501fa81a93458fa5ca84b95e8ac7f7cfecda95396c0ce9d43e38612439016df1522135480f35f4a8e9
|
data/.idea/workspace.xml
CHANGED
|
@@ -4,14 +4,7 @@
|
|
|
4
4
|
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
5
|
</component>
|
|
6
6
|
<component name="ChangeListManager">
|
|
7
|
-
<list default="true" id="1bd4c817-c426-471c-bdf5-e80649db2ae2" name="更改" comment="
|
|
8
|
-
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
9
|
-
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
|
10
|
-
<change beforePath="$PROJECT_DIR$/QUICKSTART.md" beforeDir="false" afterPath="$PROJECT_DIR$/QUICKSTART.md" afterDir="false" />
|
|
11
|
-
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
12
|
-
<change beforePath="$PROJECT_DIR$/lib/appcask/version.rb" beforeDir="false" afterPath="$PROJECT_DIR$/lib/appcask/version.rb" afterDir="false" />
|
|
13
|
-
<change beforePath="$PROJECT_DIR$/sig/appcask.rbs" beforeDir="false" afterPath="$PROJECT_DIR$/sig/appcask.rbs" afterDir="false" />
|
|
14
|
-
</list>
|
|
7
|
+
<list default="true" id="1bd4c817-c426-471c-bdf5-e80649db2ae2" name="更改" comment="update CHANGELOG.md" />
|
|
15
8
|
<option name="SHOW_DIALOG" value="false" />
|
|
16
9
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
17
10
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
@@ -80,7 +73,7 @@
|
|
|
80
73
|
<workItem from="1769697520161" duration="4958000" />
|
|
81
74
|
<workItem from="1769702829159" duration="2415000" />
|
|
82
75
|
<workItem from="1769709405343" duration="432000" />
|
|
83
|
-
<workItem from="1769754602300" duration="
|
|
76
|
+
<workItem from="1769754602300" duration="1322000" />
|
|
84
77
|
</task>
|
|
85
78
|
<task id="LOCAL-00001" summary="delete gem">
|
|
86
79
|
<option name="closed" value="true" />
|
|
@@ -98,7 +91,23 @@
|
|
|
98
91
|
<option name="project" value="LOCAL" />
|
|
99
92
|
<updated>1769709497761</updated>
|
|
100
93
|
</task>
|
|
101
|
-
<
|
|
94
|
+
<task id="LOCAL-00003" summary="Release v0.5.0">
|
|
95
|
+
<option name="closed" value="true" />
|
|
96
|
+
<created>1769755115516</created>
|
|
97
|
+
<option name="number" value="00003" />
|
|
98
|
+
<option name="presentableId" value="LOCAL-00003" />
|
|
99
|
+
<option name="project" value="LOCAL" />
|
|
100
|
+
<updated>1769755115516</updated>
|
|
101
|
+
</task>
|
|
102
|
+
<task id="LOCAL-00004" summary="update CHANGELOG.md">
|
|
103
|
+
<option name="closed" value="true" />
|
|
104
|
+
<created>1769755271793</created>
|
|
105
|
+
<option name="number" value="00004" />
|
|
106
|
+
<option name="presentableId" value="LOCAL-00004" />
|
|
107
|
+
<option name="project" value="LOCAL" />
|
|
108
|
+
<updated>1769755271793</updated>
|
|
109
|
+
</task>
|
|
110
|
+
<option name="localTasksCounter" value="5" />
|
|
102
111
|
<servers />
|
|
103
112
|
</component>
|
|
104
113
|
<component name="TypeScriptGeneratedFilesManager">
|
|
@@ -108,6 +117,8 @@
|
|
|
108
117
|
<MESSAGE value="first commit" />
|
|
109
118
|
<MESSAGE value="delete gem" />
|
|
110
119
|
<MESSAGE value="Release v0.4.0" />
|
|
111
|
-
<
|
|
120
|
+
<MESSAGE value="Release v0.5.0" />
|
|
121
|
+
<MESSAGE value="update CHANGELOG.md" />
|
|
122
|
+
<option name="LAST_COMMIT_MESSAGE" value="update CHANGELOG.md" />
|
|
112
123
|
</component>
|
|
113
124
|
</project>
|
data/CHANGELOG.md
CHANGED
|
@@ -51,3 +51,27 @@ New support added for:
|
|
|
51
51
|
|
|
52
52
|
### 📚 Documentation
|
|
53
53
|
- Update Ruby version requirement in README
|
|
54
|
+
|
|
55
|
+
## [0.5.0] - 2026-01-30
|
|
56
|
+
|
|
57
|
+
### 🌍 Internationalization
|
|
58
|
+
- **Comprehensive English Documentation**: Translated all user-facing documentation from Chinese to American English
|
|
59
|
+
- README.md: Complete translation of app description, features, installation guide, usage examples
|
|
60
|
+
- QUICKSTART.md: Full translation of quick start guide with all use case scenarios
|
|
61
|
+
- CHANGELOG.md: Translated all version history entries
|
|
62
|
+
- **Type Signature Update**: Fixed RBS type signature module name from `Appcask` to `AppCask` for consistency
|
|
63
|
+
|
|
64
|
+
## [0.6.0] - 2026-07-24
|
|
65
|
+
|
|
66
|
+
### 🆕 New Features
|
|
67
|
+
- **`appcask fetch`** command: Extract icons from locally installed macOS apps
|
|
68
|
+
- Reads `Info.plist` to find `CFBundleTypeIconFile` / `CFBundleIconFile`
|
|
69
|
+
- Parses `.icns` files and extracts the largest available PNG
|
|
70
|
+
- Auto-resizes to 512x512 with transparency preserved
|
|
71
|
+
- Saves to `~/Desktop/AppCask Downloads/Local Apps/`
|
|
72
|
+
- Supports Chinese app names
|
|
73
|
+
|
|
74
|
+
### 📦 Dependencies
|
|
75
|
+
- Add `icns` gem for `.icns` file parsing
|
|
76
|
+
- Add `CFPropertyList` gem for reading binary/XML plist files
|
|
77
|
+
- Add `mini_magick` gem for image resizing
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
@@ -74,7 +74,7 @@ git clone https://github.com/yourusername/appcask.git
|
|
|
74
74
|
cd appcask
|
|
75
75
|
bundle install
|
|
76
76
|
rake build
|
|
77
|
-
gem install pkg/appcask-
|
|
77
|
+
gem install pkg/appcask-0.6.0.gem
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
---
|
|
@@ -99,7 +99,7 @@ $ appcask "Twitter"
|
|
|
99
99
|
|
|
100
100
|
╔═══════════════════════════════════════════╗
|
|
101
101
|
║ AppCask - App Resources Downloader ║
|
|
102
|
-
║
|
|
102
|
+
║ v0.6.0 ║
|
|
103
103
|
╚═══════════════════════════════════════════╝
|
|
104
104
|
|
|
105
105
|
🔍 Searching for "Twitter"...
|
data/examples/batch_download.rb
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
2
4
|
# AppCask batch download example
|
|
3
5
|
# Usage: ruby examples/batch_download.rb
|
|
4
6
|
|
|
5
|
-
require
|
|
7
|
+
require "appcask"
|
|
6
8
|
|
|
7
9
|
# Define the list of apps to download
|
|
8
10
|
apps = [
|
|
9
|
-
{ name:
|
|
10
|
-
{ name:
|
|
11
|
-
{ name:
|
|
12
|
-
{ name:
|
|
13
|
-
{ name:
|
|
11
|
+
{ name: "Instagram", region: "us", description: "Social media app" },
|
|
12
|
+
{ name: "Twitter", region: "us", description: "Microblogging platform" },
|
|
13
|
+
{ name: "WeChat", region: "cn", description: "Instant messaging app" },
|
|
14
|
+
{ name: "LINE", region: "jp", description: "Messaging app popular in Japan" },
|
|
15
|
+
{ name: "KakaoTalk", region: "kr", description: "Messaging app popular in Korea" }
|
|
14
16
|
]
|
|
15
17
|
|
|
16
|
-
puts
|
|
17
|
-
puts
|
|
18
|
-
puts
|
|
18
|
+
puts "=" * 50
|
|
19
|
+
puts "AppCask Batch Download Script"
|
|
20
|
+
puts "=" * 50
|
|
19
21
|
puts "\nPreparing to download resources for #{apps.size} apps...\n\n"
|
|
20
22
|
|
|
21
23
|
apps.each_with_index do |app, index|
|
|
22
24
|
puts "\n[#{index + 1}/#{apps.size}] Processing: #{app[:name]} (#{app[:description]})"
|
|
23
|
-
puts
|
|
25
|
+
puts "-" * 50
|
|
24
26
|
|
|
25
27
|
# Set command-line arguments
|
|
26
28
|
ARGV.clear
|
|
@@ -38,6 +40,6 @@ apps.each_with_index do |app, index|
|
|
|
38
40
|
sleep 2 unless index == apps.size - 1
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
puts "\n"
|
|
42
|
-
puts
|
|
43
|
-
puts
|
|
43
|
+
puts "\n#{"=" * 50}"
|
|
44
|
+
puts "Batch download completed!"
|
|
45
|
+
puts "=" * 50
|
data/exe/appcask
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cfpropertylist"
|
|
4
|
+
require "icns"
|
|
5
|
+
require "mini_magick"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module AppCask
|
|
9
|
+
class LocalApp
|
|
10
|
+
APPS_DIR = "/Applications"
|
|
11
|
+
OUTPUT_BASE = File.join(Dir.home, "Desktop", "AppCask Downloads", "Local Apps")
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def fetch_all
|
|
15
|
+
puts "\nFetching local app icons..."
|
|
16
|
+
puts "Source: #{APPS_DIR}"
|
|
17
|
+
puts "Output: #{OUTPUT_BASE}\n\n"
|
|
18
|
+
|
|
19
|
+
FileUtils.mkdir_p(OUTPUT_BASE)
|
|
20
|
+
|
|
21
|
+
apps = Dir.glob(File.join(APPS_DIR, "*.app"))
|
|
22
|
+
total = apps.count
|
|
23
|
+
success = 0
|
|
24
|
+
skipped = 0
|
|
25
|
+
|
|
26
|
+
apps.each_with_index do |app_path, index|
|
|
27
|
+
app_name = File.basename(app_path, ".app")
|
|
28
|
+
print "\r[#{index + 1}/#{total}] Processing: #{app_name.ljust(30)}"
|
|
29
|
+
|
|
30
|
+
result = extract_icon(app_path)
|
|
31
|
+
if result
|
|
32
|
+
success += 1
|
|
33
|
+
else
|
|
34
|
+
skipped += 1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts "\n\n#{"=" * 50}"
|
|
39
|
+
puts "Done!"
|
|
40
|
+
puts " Total apps: #{total}"
|
|
41
|
+
puts " Extracted: #{success}"
|
|
42
|
+
puts " Skipped: #{skipped}"
|
|
43
|
+
puts " Output: #{OUTPUT_BASE}"
|
|
44
|
+
puts "=" * 50
|
|
45
|
+
|
|
46
|
+
# Open folder on macOS
|
|
47
|
+
return unless RUBY_PLATFORM.include?("darwin")
|
|
48
|
+
|
|
49
|
+
print "\nOpen the folder? (y/n): "
|
|
50
|
+
response = $stdin.gets
|
|
51
|
+
system("open '#{OUTPUT_BASE}'") if response&.strip&.downcase == "y"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def extract_icon(app_path)
|
|
57
|
+
# 1. Read Info.plist
|
|
58
|
+
plist_path = File.join(app_path, "Contents", "Info.plist")
|
|
59
|
+
return nil unless File.exist?(plist_path)
|
|
60
|
+
|
|
61
|
+
plist = CFPropertyList::List.new(file: plist_path)
|
|
62
|
+
info = CFPropertyList.native_types(plist.value)
|
|
63
|
+
|
|
64
|
+
# 2. Get icon file name
|
|
65
|
+
icon_name = info["CFBundleTypeIconFile"] || info["CFBundleIconFile"] || info["CFBundleIconName"]
|
|
66
|
+
return nil unless icon_name
|
|
67
|
+
|
|
68
|
+
icon_name += ".icns" unless icon_name.end_with?(".icns")
|
|
69
|
+
|
|
70
|
+
# 3. Find .icns file
|
|
71
|
+
icns_path = File.join(app_path, "Contents", "Resources", icon_name)
|
|
72
|
+
return nil unless File.exist?(icns_path)
|
|
73
|
+
|
|
74
|
+
# 4. Parse .icns and extract largest PNG
|
|
75
|
+
reader = Icns::Reader.new(icns_path)
|
|
76
|
+
|
|
77
|
+
# Priority: 1024 → 512 → 256 → 128
|
|
78
|
+
[1024, 512, 256, 128].each do |size|
|
|
79
|
+
data = reader.image(size: size)
|
|
80
|
+
next unless data
|
|
81
|
+
next unless png?(data)
|
|
82
|
+
|
|
83
|
+
# 5. Save as 512x512 PNG
|
|
84
|
+
output_path = output_path(app_path)
|
|
85
|
+
save_as_512(data, size, output_path)
|
|
86
|
+
return output_path
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
nil
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def png?(data)
|
|
95
|
+
png_magic = [0x89, 0x50, 0x4e, 0x47].pack("C4")
|
|
96
|
+
data[0..3] == png_magic
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def save_as_512(data, original_size, output_path)
|
|
100
|
+
if original_size == 512
|
|
101
|
+
# Direct write
|
|
102
|
+
File.write(output_path, data)
|
|
103
|
+
else
|
|
104
|
+
# Need to resize - use mini_magick
|
|
105
|
+
resize_with_mini_magick(data, output_path)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resize_with_mini_magick(data, output_path)
|
|
110
|
+
# Write to temp file, resize, then save
|
|
111
|
+
tmp_input = File.join(Dir.tmpdir, "appcask_input_#{Process.pid}.png")
|
|
112
|
+
tmp_output = File.join(Dir.tmpdir, "appcask_output_#{Process.pid}.png")
|
|
113
|
+
|
|
114
|
+
File.write(tmp_input, data)
|
|
115
|
+
|
|
116
|
+
image = MiniMagick::Image.new(tmp_input)
|
|
117
|
+
image.resize "512x512"
|
|
118
|
+
image.format "png"
|
|
119
|
+
image.write(tmp_output)
|
|
120
|
+
|
|
121
|
+
FileUtils.cp(tmp_output, output_path)
|
|
122
|
+
ensure
|
|
123
|
+
FileUtils.rm_f(tmp_input) if tmp_input
|
|
124
|
+
FileUtils.rm_f(tmp_output) if tmp_output
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def output_path(app_path)
|
|
128
|
+
app_name = File.basename(app_path, ".app")
|
|
129
|
+
filename = "#{sanitize_app_name(app_name)}.png"
|
|
130
|
+
File.join(OUTPUT_BASE, filename)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sanitize_app_name(name)
|
|
134
|
+
# Keep Chinese characters, convert to lowercase, replace spaces with hyphens
|
|
135
|
+
name.downcase.gsub(/\s+/, "-").gsub(/[^a-z0-9\u4e00-\u9fff-]/, "").gsub(/^-|-$/, "")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/appcask/version.rb
CHANGED
data/lib/appcask.rb
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "appcask/version"
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
4
|
+
require "net/https"
|
|
5
|
+
require "open-uri"
|
|
6
|
+
require "json"
|
|
7
|
+
require "fileutils"
|
|
8
8
|
|
|
9
9
|
module AppCask
|
|
10
10
|
class Error < StandardError; end
|
|
11
11
|
|
|
12
12
|
ICON_SIZES = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
"0" => { display: "60x60", key: "artworkUrl60" },
|
|
14
|
+
"1" => { display: "100x100", key: "artworkUrl100" },
|
|
15
|
+
"2" => { display: "512x512", key: "artworkUrl512" },
|
|
16
|
+
"3" => { display: "1024x1024", key: "artworkUrl512" }
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
19
|
SCREENSHOT_DEVICES = {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
"iphone" => "iPhone screenshots",
|
|
21
|
+
"ipad" => "iPad screenshots"
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
24
|
COUNTRIES = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
"us" => "United States",
|
|
26
|
+
"cn" => "China",
|
|
27
|
+
"jp" => "Japan",
|
|
28
|
+
"kr" => "South Korea",
|
|
29
|
+
"hk" => "Hong Kong",
|
|
30
|
+
"tw" => "Taiwan",
|
|
31
|
+
"gb" => "United Kingdom",
|
|
32
|
+
"de" => "Germany",
|
|
33
|
+
"fr" => "France"
|
|
34
34
|
}.freeze
|
|
35
35
|
|
|
36
36
|
DOWNLOAD_MODES = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
"1" => { name: "Icon Only", method: :download_icon },
|
|
38
|
+
"2" => { name: "Screenshots Only", method: :download_screenshots },
|
|
39
|
+
"3" => { name: "Description Only", method: :download_description },
|
|
40
|
+
"4" => { name: "All Assets", method: :download_all }
|
|
41
41
|
}.freeze
|
|
42
42
|
|
|
43
43
|
class << self
|
|
44
44
|
def main
|
|
45
|
-
|
|
45
|
+
command = ARGV[0]&.downcase
|
|
46
|
+
|
|
47
|
+
case command
|
|
48
|
+
when "fetch", "local"
|
|
49
|
+
show_banner
|
|
50
|
+
require_relative "appcask/local_app"
|
|
51
|
+
AppCask::LocalApp.fetch_all
|
|
52
|
+
else
|
|
46
53
|
get
|
|
47
|
-
rescue Interrupt
|
|
48
|
-
puts "\n\n👋 Goodbye!"
|
|
49
|
-
exit 0
|
|
50
|
-
rescue StandardError => e
|
|
51
|
-
warn "\n❌ Error: #{e.message}"
|
|
52
|
-
warn e.backtrace if ENV['DEBUG']
|
|
53
|
-
exit 1
|
|
54
54
|
end
|
|
55
|
+
rescue Interrupt
|
|
56
|
+
puts "\n\n👋 Goodbye!"
|
|
57
|
+
exit 0
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
warn "\n❌ Error: #{e.message}"
|
|
60
|
+
warn e.backtrace if ENV["DEBUG"]
|
|
61
|
+
exit 1
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
def get
|
|
@@ -64,7 +71,7 @@ module AppCask
|
|
|
64
71
|
|
|
65
72
|
results = search_app(app_name, country)
|
|
66
73
|
|
|
67
|
-
if results.nil? || results[
|
|
74
|
+
if results.nil? || results["resultCount"].to_i.zero?
|
|
68
75
|
warn "❌ No apps found for \"#{app_name}\"."
|
|
69
76
|
return
|
|
70
77
|
end
|
|
@@ -82,20 +89,20 @@ module AppCask
|
|
|
82
89
|
|
|
83
90
|
def show_banner
|
|
84
91
|
puts <<~BANNER
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
▄▖ ▄▖ ▌
|
|
93
|
+
▌▌▛▌▛▌▌ ▀▌▛▘▙▘
|
|
94
|
+
▛▌▙▌▙▌▙▖█▌▄▌▛▖
|
|
95
|
+
▌ ▌
|
|
96
|
+
|
|
97
|
+
v#{AppCask::VERSION}
|
|
91
98
|
BANNER
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
def get_app_name
|
|
95
|
-
if ARGV.
|
|
102
|
+
if ARGV.any?
|
|
96
103
|
ARGV[0]
|
|
97
104
|
else
|
|
98
|
-
print
|
|
105
|
+
print "📱 Enter the app name to search: "
|
|
99
106
|
$stdin.gets.chomp.strip
|
|
100
107
|
end
|
|
101
108
|
end
|
|
@@ -110,16 +117,21 @@ module AppCask
|
|
|
110
117
|
print "Choose one (default: us): "
|
|
111
118
|
|
|
112
119
|
input = $stdin.gets.chomp.strip.downcase
|
|
113
|
-
input.empty?
|
|
120
|
+
if input.empty?
|
|
121
|
+
"us"
|
|
122
|
+
else
|
|
123
|
+
(COUNTRIES.key?(input) ? input : "us")
|
|
124
|
+
end
|
|
114
125
|
end
|
|
126
|
+
|
|
115
127
|
def search_app(app_name, country)
|
|
116
|
-
url = URI(
|
|
128
|
+
url = URI("https://itunes.apple.com/search")
|
|
117
129
|
params = {
|
|
118
130
|
term: app_name,
|
|
119
131
|
country: country,
|
|
120
|
-
media:
|
|
121
|
-
entity:
|
|
122
|
-
limit:
|
|
132
|
+
media: "software",
|
|
133
|
+
entity: "software",
|
|
134
|
+
limit: "20"
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
http = Net::HTTP.new(url.host, url.port)
|
|
@@ -140,18 +152,17 @@ module AppCask
|
|
|
140
152
|
|
|
141
153
|
json = JSON.parse(response.body)
|
|
142
154
|
|
|
143
|
-
if json[
|
|
144
|
-
warn
|
|
155
|
+
if json["resultCount"].zero?
|
|
156
|
+
warn "😕 No apps found. Please try a different keyword."
|
|
145
157
|
return nil
|
|
146
158
|
end
|
|
147
159
|
|
|
148
160
|
json
|
|
149
|
-
|
|
150
161
|
rescue JSON::ParserError => e
|
|
151
162
|
warn "❌ Failed to parse response: #{e.message}"
|
|
152
163
|
nil
|
|
153
164
|
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
154
|
-
warn
|
|
165
|
+
warn "❌ Network timeout. Please check your connection."
|
|
155
166
|
nil
|
|
156
167
|
rescue StandardError => e
|
|
157
168
|
warn "❌ Search error: #{e.message}"
|
|
@@ -159,34 +170,34 @@ module AppCask
|
|
|
159
170
|
end
|
|
160
171
|
|
|
161
172
|
def select_app(results)
|
|
162
|
-
puts "\n📋 Found #{results[
|
|
173
|
+
puts "\n📋 Found #{results["resultCount"]} result(s):\n\n"
|
|
163
174
|
|
|
164
|
-
results[
|
|
165
|
-
developer = item[
|
|
166
|
-
price = item[
|
|
167
|
-
version = item[
|
|
168
|
-
rating = item[
|
|
175
|
+
results["results"].each_with_index do |item, index|
|
|
176
|
+
developer = item["artistName"] || "Unknown Developer"
|
|
177
|
+
price = item["formattedPrice"] || item["price"]
|
|
178
|
+
version = item["version"] || "N/A"
|
|
179
|
+
rating = item["averageUserRating"] ? "⭐ #{item["averageUserRating"].round(1)}" : "No Rating"
|
|
169
180
|
|
|
170
|
-
puts " [#{index}] #{item[
|
|
181
|
+
puts " [#{index}] #{item["trackCensoredName"]}"
|
|
171
182
|
puts " Developer: #{developer} | Version: #{version}"
|
|
172
183
|
puts " Price: #{price} | Rating: #{rating}"
|
|
173
184
|
puts
|
|
174
185
|
end
|
|
175
186
|
|
|
176
|
-
print "Select an app (0-#{results[
|
|
187
|
+
print "Select an app (0-#{results["resultCount"] - 1}, or press q to quit): "
|
|
177
188
|
input = $stdin.gets.chomp.strip
|
|
178
189
|
|
|
179
|
-
return nil if input.downcase ==
|
|
190
|
+
return nil if input.downcase == "q"
|
|
180
191
|
|
|
181
192
|
index = input.to_i
|
|
182
193
|
|
|
183
|
-
unless valid_index?(index, results[
|
|
194
|
+
unless valid_index?(index, results["resultCount"])
|
|
184
195
|
warn "❌ Invalid selection."
|
|
185
196
|
return nil
|
|
186
197
|
end
|
|
187
198
|
|
|
188
|
-
selected = results[
|
|
189
|
-
puts "\n✅ Selected: #{selected[
|
|
199
|
+
selected = results["results"][index]
|
|
200
|
+
puts "\n✅ Selected: #{selected["trackCensoredName"]}"
|
|
190
201
|
selected
|
|
191
202
|
end
|
|
192
203
|
|
|
@@ -207,7 +218,7 @@ module AppCask
|
|
|
207
218
|
DOWNLOAD_MODES[input]
|
|
208
219
|
end
|
|
209
220
|
|
|
210
|
-
def download_icon(app_info,
|
|
221
|
+
def download_icon(app_info, _country)
|
|
211
222
|
puts "\n🎨 Downloading app icon\n"
|
|
212
223
|
|
|
213
224
|
size_info = select_icon_size
|
|
@@ -221,23 +232,21 @@ module AppCask
|
|
|
221
232
|
end
|
|
222
233
|
|
|
223
234
|
# If 1024x1024 is selected, replace 512 with 1024 in the URL
|
|
224
|
-
if size_info[:display].include?(
|
|
225
|
-
artwork_url = artwork_url.gsub('512x512', '1024x1024')
|
|
226
|
-
end
|
|
235
|
+
artwork_url = artwork_url.gsub("512x512", "1024x1024") if size_info[:display].include?("1024")
|
|
227
236
|
|
|
228
237
|
puts "\n⬇️ Downloading icon..."
|
|
229
238
|
|
|
230
|
-
download_dir = create_app_directory(app_info,
|
|
231
|
-
file_name = "icon-#{size_info[:display].split
|
|
239
|
+
download_dir = create_app_directory(app_info, "icons")
|
|
240
|
+
file_name = "icon-#{size_info[:display].split.first}"
|
|
232
241
|
|
|
233
242
|
download_image(artwork_url, download_dir, file_name)
|
|
234
243
|
end
|
|
235
244
|
|
|
236
|
-
def download_screenshots(app_info,
|
|
245
|
+
def download_screenshots(app_info, _country)
|
|
237
246
|
puts "\n📸 Downloading app screenshots\n"
|
|
238
247
|
|
|
239
|
-
iphone_urls = app_info[
|
|
240
|
-
ipad_urls = app_info[
|
|
248
|
+
iphone_urls = app_info["screenshotUrls"] || []
|
|
249
|
+
ipad_urls = app_info["ipadScreenshotUrls"] || []
|
|
241
250
|
|
|
242
251
|
if iphone_urls.empty? && ipad_urls.empty?
|
|
243
252
|
puts "❌ This app does not provide any screenshots."
|
|
@@ -250,25 +259,25 @@ module AppCask
|
|
|
250
259
|
|
|
251
260
|
print "\nWhich device screenshots would you like to download? (iphone/ipad/all, default: all): "
|
|
252
261
|
device = $stdin.gets.chomp.strip.downcase
|
|
253
|
-
device =
|
|
262
|
+
device = "all" if device.empty?
|
|
254
263
|
|
|
255
|
-
download_dir = create_app_directory(app_info,
|
|
264
|
+
download_dir = create_app_directory(app_info, "screenshots")
|
|
256
265
|
|
|
257
266
|
case device
|
|
258
|
-
when
|
|
259
|
-
download_screenshot_set(iphone_urls, download_dir,
|
|
260
|
-
when
|
|
261
|
-
download_screenshot_set(ipad_urls, download_dir,
|
|
267
|
+
when "iphone"
|
|
268
|
+
download_screenshot_set(iphone_urls, download_dir, "iPhone")
|
|
269
|
+
when "ipad"
|
|
270
|
+
download_screenshot_set(ipad_urls, download_dir, "iPad")
|
|
262
271
|
else
|
|
263
|
-
download_screenshot_set(iphone_urls, download_dir,
|
|
264
|
-
download_screenshot_set(ipad_urls, download_dir,
|
|
272
|
+
download_screenshot_set(iphone_urls, download_dir, "iPhone") unless iphone_urls.empty?
|
|
273
|
+
download_screenshot_set(ipad_urls, download_dir, "iPad") unless ipad_urls.empty?
|
|
265
274
|
end
|
|
266
275
|
end
|
|
267
276
|
|
|
268
|
-
def download_description(app_info,
|
|
277
|
+
def download_description(app_info, _country)
|
|
269
278
|
puts "\n📝 Saving app information\n"
|
|
270
279
|
|
|
271
|
-
download_dir = create_app_directory(app_info,
|
|
280
|
+
download_dir = create_app_directory(app_info, "info")
|
|
272
281
|
|
|
273
282
|
# Generate detailed text information
|
|
274
283
|
info_text = generate_app_info_text(app_info)
|
|
@@ -278,16 +287,16 @@ module AppCask
|
|
|
278
287
|
|
|
279
288
|
# Save as TXT
|
|
280
289
|
txt_file = File.join(download_dir, "app_info.txt")
|
|
281
|
-
File.open(txt_file,
|
|
290
|
+
File.open(txt_file, "w:UTF-8") { |f| f.write(info_text) }
|
|
282
291
|
|
|
283
292
|
# Save as JSON
|
|
284
293
|
json_file = File.join(download_dir, "app_info.json")
|
|
285
|
-
File.open(json_file,
|
|
294
|
+
File.open(json_file, "w:UTF-8") { |f| f.write(JSON.pretty_generate(info_json)) }
|
|
286
295
|
|
|
287
296
|
# Save as Markdown
|
|
288
297
|
md_file = File.join(download_dir, "README.md")
|
|
289
298
|
md_text = generate_app_info_markdown(app_info)
|
|
290
|
-
File.open(md_file,
|
|
299
|
+
File.open(md_file, "w:UTF-8") { |f| f.write(md_text) }
|
|
291
300
|
|
|
292
301
|
puts "✨ App information saved successfully!"
|
|
293
302
|
puts "📁 Directory: #{download_dir}"
|
|
@@ -296,48 +305,48 @@ module AppCask
|
|
|
296
305
|
puts " - README.md (Markdown format)"
|
|
297
306
|
|
|
298
307
|
# On macOS, ask whether to open the directory
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
return unless RUBY_PLATFORM.include?("darwin")
|
|
309
|
+
|
|
310
|
+
print "\nOpen the folder now? (y/n): "
|
|
311
|
+
response = $stdin.gets.chomp.strip.downcase
|
|
312
|
+
system("open '#{download_dir}'") if response == "y"
|
|
304
313
|
end
|
|
305
314
|
|
|
306
|
-
def download_all(app_info,
|
|
315
|
+
def download_all(app_info, _country)
|
|
307
316
|
puts "\n📦 Downloading full package (icons + screenshots + app info)\n"
|
|
308
317
|
|
|
309
318
|
base_dir = create_app_directory(app_info)
|
|
310
319
|
|
|
311
320
|
# 1. Download all icon sizes
|
|
312
321
|
puts "\n[1/3] 📥 Downloading icons..."
|
|
313
|
-
icon_dir = File.join(base_dir,
|
|
322
|
+
icon_dir = File.join(base_dir, "icons")
|
|
314
323
|
FileUtils.mkdir_p(icon_dir)
|
|
315
324
|
|
|
316
|
-
ICON_SIZES.
|
|
325
|
+
ICON_SIZES.each_value do |size_info|
|
|
317
326
|
artwork_url = app_info[size_info[:key]]
|
|
318
327
|
next unless artwork_url
|
|
319
328
|
|
|
320
329
|
url =
|
|
321
|
-
if size_info[:display].include?(
|
|
322
|
-
artwork_url.gsub(
|
|
330
|
+
if size_info[:display].include?("1024")
|
|
331
|
+
artwork_url.gsub("512x512", "1024x1024")
|
|
323
332
|
else
|
|
324
333
|
artwork_url
|
|
325
334
|
end
|
|
326
335
|
|
|
327
|
-
filename = "icon-#{size_info[:display].split
|
|
336
|
+
filename = "icon-#{size_info[:display].split.first}"
|
|
328
337
|
download_image(url, icon_dir, filename, silent: true)
|
|
329
338
|
end
|
|
330
339
|
|
|
331
340
|
# 2. Download all screenshots
|
|
332
341
|
puts "\n[2/3] 📥 Downloading screenshots..."
|
|
333
|
-
screenshot_dir = File.join(base_dir,
|
|
342
|
+
screenshot_dir = File.join(base_dir, "screenshots")
|
|
334
343
|
FileUtils.mkdir_p(screenshot_dir)
|
|
335
344
|
|
|
336
|
-
iphone_urls = app_info[
|
|
337
|
-
ipad_urls = app_info[
|
|
345
|
+
iphone_urls = app_info["screenshotUrls"] || []
|
|
346
|
+
ipad_urls = app_info["ipadScreenshotUrls"] || []
|
|
338
347
|
|
|
339
|
-
download_screenshot_set(iphone_urls, screenshot_dir,
|
|
340
|
-
download_screenshot_set(ipad_urls, screenshot_dir,
|
|
348
|
+
download_screenshot_set(iphone_urls, screenshot_dir, "iPhone", silent: true) unless iphone_urls.empty?
|
|
349
|
+
download_screenshot_set(ipad_urls, screenshot_dir, "iPad", silent: true) unless ipad_urls.empty?
|
|
341
350
|
|
|
342
351
|
# 3. Save app information
|
|
343
352
|
puts "\n[3/3] 📥 Saving app information..."
|
|
@@ -346,9 +355,9 @@ module AppCask
|
|
|
346
355
|
info_json = generate_app_info_json(app_info)
|
|
347
356
|
info_md = generate_app_info_markdown(app_info)
|
|
348
357
|
|
|
349
|
-
File.open(File.join(base_dir,
|
|
350
|
-
File.open(File.join(base_dir,
|
|
351
|
-
File.open(File.join(base_dir,
|
|
358
|
+
File.open(File.join(base_dir, "app_info.txt"), "w:UTF-8") { |f| f.write(info_text) }
|
|
359
|
+
File.open(File.join(base_dir, "app_info.json"), "w:UTF-8") { |f| f.write(JSON.pretty_generate(info_json)) }
|
|
360
|
+
File.open(File.join(base_dir, "README.md"), "w:UTF-8") { |f| f.write(info_md) }
|
|
352
361
|
|
|
353
362
|
puts "\n✨ Download completed!"
|
|
354
363
|
puts "📁 All files saved to: #{base_dir}"
|
|
@@ -361,18 +370,18 @@ module AppCask
|
|
|
361
370
|
puts " └── README.md (Markdown)"
|
|
362
371
|
|
|
363
372
|
# Statistics
|
|
364
|
-
files = Dir.glob(File.join(base_dir,
|
|
373
|
+
files = Dir.glob(File.join(base_dir, "**", "*")).select { |f| File.file?(f) }
|
|
365
374
|
total_files = files.count
|
|
366
375
|
total_size = (files.sum { |f| File.size(f) } / 1024.0 / 1024.0).round(2)
|
|
367
376
|
|
|
368
377
|
puts "\n📊 Summary: #{total_files} files, total size #{total_size} MB"
|
|
369
378
|
|
|
370
379
|
# On macOS, ask whether to open the directory
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
return unless RUBY_PLATFORM.include?("darwin")
|
|
381
|
+
|
|
382
|
+
print "\nOpen the folder now? (y/n): "
|
|
383
|
+
response = $stdin.gets.chomp.strip.downcase
|
|
384
|
+
system("open '#{base_dir}'") if response == "y"
|
|
376
385
|
end
|
|
377
386
|
|
|
378
387
|
def select_icon_size
|
|
@@ -381,7 +390,7 @@ module AppCask
|
|
|
381
390
|
|
|
382
391
|
print "\nSelect (0-3, default: 2): "
|
|
383
392
|
input = $stdin.gets.chomp.strip
|
|
384
|
-
input =
|
|
393
|
+
input = "2" if input.empty?
|
|
385
394
|
|
|
386
395
|
unless ICON_SIZES.key?(input)
|
|
387
396
|
warn "❌ Invalid selection."
|
|
@@ -417,7 +426,7 @@ module AppCask
|
|
|
417
426
|
filename = File.join(dir, "#{basename}.#{ext}")
|
|
418
427
|
filename = get_unique_filename(filename)
|
|
419
428
|
|
|
420
|
-
File.
|
|
429
|
+
File.binwrite(filename, content)
|
|
421
430
|
|
|
422
431
|
unless silent
|
|
423
432
|
file_size = (File.size(filename) / 1024.0).round(2)
|
|
@@ -430,99 +439,99 @@ module AppCask
|
|
|
430
439
|
|
|
431
440
|
def generate_app_info_text(app_info)
|
|
432
441
|
<<~INFO
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
442
|
+
===================================================
|
|
443
|
+
APPLICATION DETAILS
|
|
444
|
+
===================================================
|
|
445
|
+
|
|
446
|
+
[Basic Information]
|
|
447
|
+
App Name: #{app_info["trackCensoredName"]}
|
|
448
|
+
App ID: #{app_info["trackId"]}
|
|
449
|
+
Bundle ID: #{app_info["bundleId"]}
|
|
450
|
+
Developer: #{app_info["artistName"]}
|
|
451
|
+
Developer ID: #{app_info["artistId"]}
|
|
452
|
+
|
|
453
|
+
[Version Information]
|
|
454
|
+
Current Version: #{app_info["version"]}
|
|
455
|
+
File Size: #{(app_info["fileSizeBytes"].to_i / 1024.0 / 1024.0).round(2)} MB
|
|
456
|
+
Minimum OS Requirement: iOS #{app_info["minimumOsVersion"]}
|
|
457
|
+
Supported Devices: #{app_info["supportedDevices"]&.join(", ") || "N/A"}
|
|
458
|
+
|
|
459
|
+
[Pricing & Ratings]
|
|
460
|
+
Price: #{app_info["formattedPrice"] || app_info["price"]}
|
|
461
|
+
Currency: #{app_info["currency"]}
|
|
462
|
+
Rating: #{app_info["averageUserRating"] || "N/A"} (#{app_info["userRatingCount"] || 0} ratings)
|
|
463
|
+
|
|
464
|
+
[Categories]
|
|
465
|
+
Primary Category: #{app_info["primaryGenreName"]}
|
|
466
|
+
All Categories: #{app_info["genres"]&.join(", ")}
|
|
467
|
+
|
|
468
|
+
[Release Information]
|
|
469
|
+
First Released: #{app_info["releaseDate"]}
|
|
470
|
+
Last Updated: #{app_info["currentVersionReleaseDate"]}
|
|
471
|
+
Content Rating: #{app_info["contentAdvisoryRating"]}
|
|
472
|
+
|
|
473
|
+
[Description]
|
|
474
|
+
#{app_info["description"]}
|
|
475
|
+
|
|
476
|
+
[Release Notes]
|
|
477
|
+
#{app_info["releaseNotes"] || "No release notes provided."}
|
|
478
|
+
|
|
479
|
+
[Developer Information]
|
|
480
|
+
Developer Website: #{app_info["sellerUrl"]}
|
|
481
|
+
|
|
482
|
+
[Store Link]
|
|
483
|
+
App Store: #{app_info["trackViewUrl"]}
|
|
484
|
+
|
|
485
|
+
===================================================
|
|
486
|
+
Exported At: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
|
|
487
|
+
===================================================
|
|
488
|
+
INFO
|
|
480
489
|
end
|
|
481
490
|
|
|
482
491
|
def generate_app_info_json(app_info)
|
|
483
492
|
{
|
|
484
493
|
basic: {
|
|
485
|
-
name: app_info[
|
|
486
|
-
app_id: app_info[
|
|
487
|
-
bundle_id: app_info[
|
|
488
|
-
developer: app_info[
|
|
489
|
-
developer_id: app_info[
|
|
494
|
+
name: app_info["trackCensoredName"],
|
|
495
|
+
app_id: app_info["trackId"],
|
|
496
|
+
bundle_id: app_info["bundleId"],
|
|
497
|
+
developer: app_info["artistName"],
|
|
498
|
+
developer_id: app_info["artistId"]
|
|
490
499
|
},
|
|
491
500
|
version: {
|
|
492
|
-
current_version: app_info[
|
|
493
|
-
file_size_bytes: app_info[
|
|
494
|
-
file_size_mb: (app_info[
|
|
495
|
-
minimum_os_version: app_info[
|
|
496
|
-
supported_devices: app_info[
|
|
501
|
+
current_version: app_info["version"],
|
|
502
|
+
file_size_bytes: app_info["fileSizeBytes"],
|
|
503
|
+
file_size_mb: (app_info["fileSizeBytes"].to_i / 1024.0 / 1024.0).round(2),
|
|
504
|
+
minimum_os_version: app_info["minimumOsVersion"],
|
|
505
|
+
supported_devices: app_info["supportedDevices"]
|
|
497
506
|
},
|
|
498
507
|
pricing: {
|
|
499
|
-
price: app_info[
|
|
500
|
-
formatted_price: app_info[
|
|
501
|
-
currency: app_info[
|
|
508
|
+
price: app_info["price"],
|
|
509
|
+
formatted_price: app_info["formattedPrice"],
|
|
510
|
+
currency: app_info["currency"]
|
|
502
511
|
},
|
|
503
512
|
ratings: {
|
|
504
|
-
average_rating: app_info[
|
|
505
|
-
rating_count: app_info[
|
|
506
|
-
rating_count_current_version: app_info[
|
|
513
|
+
average_rating: app_info["averageUserRating"],
|
|
514
|
+
rating_count: app_info["userRatingCount"],
|
|
515
|
+
rating_count_current_version: app_info["userRatingCountForCurrentVersion"]
|
|
507
516
|
},
|
|
508
517
|
categories: {
|
|
509
|
-
primary_genre: app_info[
|
|
510
|
-
all_genres: app_info[
|
|
518
|
+
primary_genre: app_info["primaryGenreName"],
|
|
519
|
+
all_genres: app_info["genres"]
|
|
511
520
|
},
|
|
512
521
|
release: {
|
|
513
|
-
release_date: app_info[
|
|
514
|
-
current_version_release_date: app_info[
|
|
515
|
-
content_rating: app_info[
|
|
522
|
+
release_date: app_info["releaseDate"],
|
|
523
|
+
current_version_release_date: app_info["currentVersionReleaseDate"],
|
|
524
|
+
content_rating: app_info["contentAdvisoryRating"]
|
|
516
525
|
},
|
|
517
|
-
description: app_info[
|
|
518
|
-
release_notes: app_info[
|
|
526
|
+
description: app_info["description"],
|
|
527
|
+
release_notes: app_info["releaseNotes"],
|
|
519
528
|
urls: {
|
|
520
|
-
app_store: app_info[
|
|
521
|
-
developer_website: app_info[
|
|
529
|
+
app_store: app_info["trackViewUrl"],
|
|
530
|
+
developer_website: app_info["sellerUrl"]
|
|
522
531
|
},
|
|
523
532
|
screenshots: {
|
|
524
|
-
iphone: app_info[
|
|
525
|
-
ipad: app_info[
|
|
533
|
+
iphone: app_info["screenshotUrls"],
|
|
534
|
+
ipad: app_info["ipadScreenshotUrls"]
|
|
526
535
|
},
|
|
527
536
|
exported_at: Time.now.iso8601
|
|
528
537
|
}
|
|
@@ -530,76 +539,77 @@ module AppCask
|
|
|
530
539
|
|
|
531
540
|
def generate_app_info_markdown(app_info)
|
|
532
541
|
<<~MARKDOWN
|
|
533
|
-
|
|
542
|
+
# #{app_info["trackCensoredName"]}
|
|
534
543
|
|
|
535
|
-
|
|
544
|
+
> Developer: #{app_info["artistName"]}
|
|
536
545
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
546
|
+

|
|
547
|
+

|
|
548
|
+
.gsub("-", "--")}-orange)
|
|
540
549
|
|
|
541
|
-
|
|
550
|
+
## 📱 Basic Information
|
|
542
551
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
| Item | Value |
|
|
553
|
+
|------|-------|
|
|
554
|
+
| App ID | #{app_info["trackId"]} |
|
|
555
|
+
| Bundle ID | #{app_info["bundleId"]} |
|
|
556
|
+
| Developer | #{app_info["artistName"]} |
|
|
557
|
+
| Primary Category | #{app_info["primaryGenreName"]} |
|
|
558
|
+
| Content Rating | #{app_info["contentAdvisoryRating"]} |
|
|
550
559
|
|
|
551
|
-
|
|
560
|
+
## 📊 Version Information
|
|
552
561
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
562
|
+
- **Current Version**: #{app_info["version"]}
|
|
563
|
+
- **File Size**: #{(app_info["fileSizeBytes"].to_i / 1024.0 / 1024.0).round(2)} MB
|
|
564
|
+
- **Minimum OS**: iOS #{app_info["minimumOsVersion"]}
|
|
565
|
+
- **Release Date**: #{app_info["currentVersionReleaseDate"]}
|
|
557
566
|
|
|
558
|
-
|
|
567
|
+
## ⭐ Ratings
|
|
559
568
|
|
|
560
|
-
|
|
561
|
-
|
|
569
|
+
- **Average Rating**: #{app_info["averageUserRating"] || "N/A"} / 5.0
|
|
570
|
+
- **Total Ratings**: #{app_info["userRatingCount"] || 0}
|
|
562
571
|
|
|
563
|
-
|
|
572
|
+
## 📝 Description
|
|
564
573
|
|
|
565
|
-
|
|
574
|
+
#{app_info["description"]}
|
|
566
575
|
|
|
567
|
-
|
|
576
|
+
## 🆕 What's New
|
|
568
577
|
|
|
569
|
-
|
|
578
|
+
#{app_info["releaseNotes"] || "No release notes provided."}
|
|
570
579
|
|
|
571
|
-
|
|
580
|
+
## 🔗 Links
|
|
572
581
|
|
|
573
|
-
|
|
574
|
-
|
|
582
|
+
- [App Store](#{app_info["trackViewUrl"]})
|
|
583
|
+
- [Developer Website](#{app_info["sellerUrl"]})
|
|
575
584
|
|
|
576
|
-
|
|
585
|
+
---
|
|
577
586
|
|
|
578
|
-
|
|
579
|
-
|
|
587
|
+
*Exported at: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}*
|
|
588
|
+
MARKDOWN
|
|
580
589
|
end
|
|
581
590
|
|
|
582
591
|
def detect_image_extension(content)
|
|
583
|
-
return
|
|
584
|
-
return
|
|
585
|
-
return
|
|
586
|
-
return
|
|
587
|
-
|
|
592
|
+
return "png" if content[0..7] == "\x89PNG\r\n\x1A\n".b
|
|
593
|
+
return "jpg" if content[0..1] == "\xFF\xD8".b
|
|
594
|
+
return "gif" if content[0..2] == "GIF".b
|
|
595
|
+
return "webp" if content[8..11] == "WEBP".b
|
|
596
|
+
|
|
597
|
+
"jpg"
|
|
588
598
|
end
|
|
589
599
|
|
|
590
600
|
def sanitize_filename(filename)
|
|
591
|
-
filename.gsub(
|
|
601
|
+
filename.gsub(%r{[/\\:*?"<>|]}, "_").strip
|
|
592
602
|
end
|
|
593
603
|
|
|
594
604
|
def create_app_directory(app_info, subdir = nil)
|
|
595
|
-
app_name = sanitize_filename(app_info[
|
|
605
|
+
app_name = sanitize_filename(app_info["trackCensoredName"])
|
|
596
606
|
|
|
597
|
-
desktop = File.join(Dir.home,
|
|
598
|
-
if Dir.exist?(desktop)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
607
|
+
desktop = File.join(Dir.home, "Desktop")
|
|
608
|
+
base_dir = if Dir.exist?(desktop)
|
|
609
|
+
File.join(desktop, "AppCask Downloads", app_name)
|
|
610
|
+
else
|
|
611
|
+
File.join(Dir.pwd, "AppCask Downloads", app_name)
|
|
612
|
+
end
|
|
603
613
|
|
|
604
614
|
final_dir = subdir ? File.join(base_dir, subdir) : base_dir
|
|
605
615
|
FileUtils.mkdir_p(final_dir)
|
|
@@ -610,13 +620,14 @@ module AppCask
|
|
|
610
620
|
return filename unless File.exist?(filename)
|
|
611
621
|
|
|
612
622
|
dir = File.dirname(filename)
|
|
613
|
-
basename = File.basename(filename,
|
|
623
|
+
basename = File.basename(filename, ".*")
|
|
614
624
|
ext = File.extname(filename)
|
|
615
625
|
|
|
616
626
|
counter = 1
|
|
617
627
|
loop do
|
|
618
628
|
new_filename = File.join(dir, "#{basename}_#{counter}#{ext}")
|
|
619
629
|
return new_filename unless File.exist?(new_filename)
|
|
630
|
+
|
|
620
631
|
counter += 1
|
|
621
632
|
end
|
|
622
633
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: appcask
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Billow Wang
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: CFPropertyList
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: icns
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: mini_magick
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '4.0'
|
|
12
54
|
description: AppCask is a CLI tool for downloading App Store icons, screenshots and
|
|
13
55
|
metadata.
|
|
14
56
|
email:
|
|
@@ -33,6 +75,7 @@ files:
|
|
|
33
75
|
- examples/batch_download.rb
|
|
34
76
|
- exe/appcask
|
|
35
77
|
- lib/appcask.rb
|
|
78
|
+
- lib/appcask/local_app.rb
|
|
36
79
|
- lib/appcask/version.rb
|
|
37
80
|
- sig/appcask.rbs
|
|
38
81
|
homepage: https://github.com/gamepunk/appcask
|
|
@@ -43,6 +86,7 @@ metadata:
|
|
|
43
86
|
homepage_uri: https://github.com/gamepunk/appcask
|
|
44
87
|
source_code_uri: https://github.com/gamepunk/appcask
|
|
45
88
|
changelog_uri: https://github.com/gamepunk/appcask/blob/main/CHANGELOG.md
|
|
89
|
+
rubygems_mfa_required: 'true'
|
|
46
90
|
rdoc_options: []
|
|
47
91
|
require_paths:
|
|
48
92
|
- lib
|
|
@@ -50,14 +94,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
50
94
|
requirements:
|
|
51
95
|
- - ">="
|
|
52
96
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 3.
|
|
97
|
+
version: 3.4.10
|
|
54
98
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
99
|
requirements:
|
|
56
100
|
- - ">="
|
|
57
101
|
- !ruby/object:Gem::Version
|
|
58
102
|
version: '0'
|
|
59
103
|
requirements: []
|
|
60
|
-
rubygems_version:
|
|
104
|
+
rubygems_version: 3.6.9
|
|
61
105
|
specification_version: 4
|
|
62
106
|
summary: Download App Store app assets easily
|
|
63
107
|
test_files: []
|