appcask 0.1.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 +7 -0
- data/.idea/workspace.xml +86 -0
- data/CHANGELOG.md +29 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +249 -0
- data/Rakefile +12 -0
- data/examples/batch_download.rb +43 -0
- data/lib/appcask/version.rb +5 -0
- data/lib/appcask.rb +627 -0
- data/sig/appcask.rbs +4 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 84b4342166055f34caca13c015782050cc53a3b822def01f171d5b7a3b46cfd6
|
|
4
|
+
data.tar.gz: 2d3a8ae4795e748adcc170cb9a718604fe121b1342a87dc562a6c7cf7db62ad2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d352aaccc7f028d535de981d1381d924f9e01c572cd5ccebd37950ce645625f22c2b38cd0eb0aa75cc8c6fbaec0b930d6659e3e0e0438323b1af8a86460f1685
|
|
7
|
+
data.tar.gz: e7f972f9abfafb77b5e8bb62af302b51eb61e3e0702a08979941265f7a52d7dda260a848c6d2eca8448d4bfd5fa7d5ec207c2b4ebe60e43bf74c89a06f41ab00
|
data/.idea/workspace.xml
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="1bd4c817-c426-471c-bdf5-e80649db2ae2" name="更改" comment="">
|
|
8
|
+
<change afterPath="$PROJECT_DIR$/.github/workflows/main.yml" afterDir="false" />
|
|
9
|
+
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
|
10
|
+
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
11
|
+
<change afterPath="$PROJECT_DIR$/.rubocop.yml" afterDir="false" />
|
|
12
|
+
<change afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
|
13
|
+
<change afterPath="$PROJECT_DIR$/CODE_OF_CONDUCT.md" afterDir="false" />
|
|
14
|
+
<change afterPath="$PROJECT_DIR$/Gemfile" afterDir="false" />
|
|
15
|
+
<change afterPath="$PROJECT_DIR$/LICENSE.txt" afterDir="false" />
|
|
16
|
+
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
17
|
+
<change afterPath="$PROJECT_DIR$/Rakefile" afterDir="false" />
|
|
18
|
+
<change afterPath="$PROJECT_DIR$/appcask.gemspec" afterDir="false" />
|
|
19
|
+
<change afterPath="$PROJECT_DIR$/bin/console" afterDir="false" />
|
|
20
|
+
<change afterPath="$PROJECT_DIR$/bin/setup" afterDir="false" />
|
|
21
|
+
<change afterPath="$PROJECT_DIR$/examples/batch_download.rb" afterDir="false" />
|
|
22
|
+
<change afterPath="$PROJECT_DIR$/lib/appcask.rb" afterDir="false" />
|
|
23
|
+
<change afterPath="$PROJECT_DIR$/lib/appcask/version.rb" afterDir="false" />
|
|
24
|
+
<change afterPath="$PROJECT_DIR$/sig/appcask.rbs" afterDir="false" />
|
|
25
|
+
<change afterPath="$PROJECT_DIR$/test/test_appcask.rb" afterDir="false" />
|
|
26
|
+
<change afterPath="$PROJECT_DIR$/test/test_helper.rb" afterDir="false" />
|
|
27
|
+
</list>
|
|
28
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
29
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
30
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
31
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
32
|
+
</component>
|
|
33
|
+
<component name="Git.Settings">
|
|
34
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
35
|
+
</component>
|
|
36
|
+
<component name="ProjectColorInfo"><![CDATA[{
|
|
37
|
+
"associatedIndex": 3
|
|
38
|
+
}]]></component>
|
|
39
|
+
<component name="ProjectId" id="38w4u8bpa7evnl9Tft1pg9mpNsJ" />
|
|
40
|
+
<component name="ProjectViewState">
|
|
41
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
42
|
+
<option name="showLibraryContents" value="true" />
|
|
43
|
+
</component>
|
|
44
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
45
|
+
"keyToString": {
|
|
46
|
+
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
|
47
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
48
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
49
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
50
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
51
|
+
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
52
|
+
"git-widget-placeholder": "main",
|
|
53
|
+
"node.js.detected.package.eslint": "true",
|
|
54
|
+
"node.js.detected.package.tslint": "true",
|
|
55
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
56
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
57
|
+
"nodejs_package_manager_path": "npm",
|
|
58
|
+
"ruby.structure.view.model.defaults.configured": "true",
|
|
59
|
+
"settings.editor.selected.configurable": "preferences.lookFeel",
|
|
60
|
+
"vue.rearranger.settings.migration": "true"
|
|
61
|
+
}
|
|
62
|
+
}]]></component>
|
|
63
|
+
<component name="SharedIndexes">
|
|
64
|
+
<attachedChunks>
|
|
65
|
+
<set>
|
|
66
|
+
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-RM-253.30387.79" />
|
|
67
|
+
</set>
|
|
68
|
+
</attachedChunks>
|
|
69
|
+
</component>
|
|
70
|
+
<component name="SpringUtil" SPRING_PRE_LOADER_OPTION="true" RAKE_SPRING_PRE_LOADER_OPTION="true" RAILS_SPRING_PRE_LOADER_OPTION="true" />
|
|
71
|
+
<component name="TaskManager">
|
|
72
|
+
<task active="true" id="Default" summary="Default task">
|
|
73
|
+
<changelist id="1bd4c817-c426-471c-bdf5-e80649db2ae2" name="更改" comment="" />
|
|
74
|
+
<created>1767270112011</created>
|
|
75
|
+
<option name="number" value="Default" />
|
|
76
|
+
<option name="presentableId" value="Default" />
|
|
77
|
+
<updated>1767270112011</updated>
|
|
78
|
+
<workItem from="1767270116395" duration="20000" />
|
|
79
|
+
<workItem from="1769697520161" duration="3786000" />
|
|
80
|
+
</task>
|
|
81
|
+
<servers />
|
|
82
|
+
</component>
|
|
83
|
+
<component name="TypeScriptGeneratedFilesManager">
|
|
84
|
+
<option name="version" value="3" />
|
|
85
|
+
</component>
|
|
86
|
+
</project>
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-01-29
|
|
6
|
+
|
|
7
|
+
### 🎉 AppCask 首次发布
|
|
8
|
+
|
|
9
|
+
#### 🆕 下载模式
|
|
10
|
+
- ✨ **图标下载** - 4 种尺寸可选 (60x60 到 1024x1024)
|
|
11
|
+
- 📸 **截图下载** - 支持 iPhone 和 iPad 截图批量下载
|
|
12
|
+
- 📝 **应用信息导出** - 3 种格式 (TXT/JSON/Markdown)
|
|
13
|
+
- 📦 **完整包下载** - 一键下载所有资源
|
|
14
|
+
|
|
15
|
+
#### 📋 应用信息字段
|
|
16
|
+
- 基本信息:名称、ID、Bundle ID、开发者
|
|
17
|
+
- 版本信息:版本号、文件大小、系统要求、支持设备
|
|
18
|
+
- 评分数据:平均评分、评分数量
|
|
19
|
+
- 价格信息:价格、货币
|
|
20
|
+
- 分类信息:主分类、所有分类
|
|
21
|
+
- 内容信息:应用描述、版本更新说明
|
|
22
|
+
- 链接信息:App Store 链接、开发者网站
|
|
23
|
+
|
|
24
|
+
#### 🌍 更多区域
|
|
25
|
+
新增支持:
|
|
26
|
+
- 🇹🇼 台湾 (tw)
|
|
27
|
+
- 🇬🇧 英国 (gb)
|
|
28
|
+
- 🇩🇪 德国 (de)
|
|
29
|
+
- 🇫🇷 法国 (fr)
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"appcask" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["TODO: Write your email address"](mailto:"TODO: Write your email address").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Billow Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# AppCask - App Store 资源下载工具
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
一个全能的命令行工具,用于下载 iOS App Store 的应用资源
|
|
10
|
+
**图标 · 截图 · 应用简介 · 一键打包**
|
|
11
|
+
|
|
12
|
+
[功能特性](#-功能特性) • [安装](#-安装) • [使用指南](#-使用指南) • [示例](#-使用示例)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## ✨ 功能特性
|
|
19
|
+
|
|
20
|
+
### 🎨 图标下载
|
|
21
|
+
- 支持 4 种尺寸:60x60、100x100、512x512、1024x1024
|
|
22
|
+
- 自动检测图片格式 (PNG/JPG/GIF/WEBP)
|
|
23
|
+
- 智能文件命名
|
|
24
|
+
|
|
25
|
+
### 📸 截图下载
|
|
26
|
+
- iPhone 截图
|
|
27
|
+
- iPad 截图
|
|
28
|
+
- 批量下载所有截图
|
|
29
|
+
- 自动分类保存
|
|
30
|
+
|
|
31
|
+
### 📝 应用信息导出
|
|
32
|
+
- **TXT 格式** - 易读的文本文件
|
|
33
|
+
- **JSON 格式** - 结构化数据
|
|
34
|
+
- **Markdown 格式** - 精美的文档
|
|
35
|
+
|
|
36
|
+
包含信息:
|
|
37
|
+
- 基本信息(名称、开发者、Bundle ID)
|
|
38
|
+
- 版本信息(当前版本、文件大小、系统要求)
|
|
39
|
+
- 评分统计(平均分、评分数量)
|
|
40
|
+
- 价格信息
|
|
41
|
+
- 应用描述
|
|
42
|
+
- 版本更新说明
|
|
43
|
+
- 相关链接
|
|
44
|
+
|
|
45
|
+
### 📦 一键完整包
|
|
46
|
+
下载应用的所有资源,包括:
|
|
47
|
+
- 所有尺寸的图标
|
|
48
|
+
- 所有设备的截图
|
|
49
|
+
- 完整的应用信息(3种格式)
|
|
50
|
+
|
|
51
|
+
### 🌍 多区域支持
|
|
52
|
+
- 🇺🇸 美国 (us)
|
|
53
|
+
- 🇨🇳 中国 (cn)
|
|
54
|
+
- 🇯🇵 日本 (jp)
|
|
55
|
+
- 🇰🇷 韩国 (kr)
|
|
56
|
+
- 🇭🇰 香港 (hk)
|
|
57
|
+
- 🇹🇼 台湾 (tw)
|
|
58
|
+
- 🇬🇧 英国 (gb)
|
|
59
|
+
- 🇩🇪 德国 (de)
|
|
60
|
+
- 🇫🇷 法国 (fr)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📦 安装
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gem install appcask
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
或从源码安装:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/yourusername/appcask.git
|
|
74
|
+
cd appcask
|
|
75
|
+
bundle install
|
|
76
|
+
rake build
|
|
77
|
+
gem install pkg/appcask-1.0.0.gem
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🚀 快速开始
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 交互模式
|
|
86
|
+
appcask
|
|
87
|
+
|
|
88
|
+
# 快速搜索
|
|
89
|
+
appcask "Instagram"
|
|
90
|
+
|
|
91
|
+
# 指定区域
|
|
92
|
+
appcask "微信" cn
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 完整演示
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
$ appcask "Twitter"
|
|
99
|
+
|
|
100
|
+
╔═══════════════════════════════════════════╗
|
|
101
|
+
║ AppCask - App 资源下载工具 ║
|
|
102
|
+
║ v1.0.0 ║
|
|
103
|
+
╚═══════════════════════════════════════════╝
|
|
104
|
+
|
|
105
|
+
🔍 正在搜索 "Twitter"...
|
|
106
|
+
|
|
107
|
+
📋 找到 3 个结果:
|
|
108
|
+
|
|
109
|
+
[0] X
|
|
110
|
+
开发者: X Corp. | 版本: 10.31
|
|
111
|
+
价格: Free | 评分: ⭐ 4.2
|
|
112
|
+
|
|
113
|
+
请选择 (0-2, 或按q退出): 0
|
|
114
|
+
|
|
115
|
+
✅ 已选择: X
|
|
116
|
+
|
|
117
|
+
📦 选择下载内容:
|
|
118
|
+
[1] 图标
|
|
119
|
+
[2] 截图
|
|
120
|
+
[3] 简介信息
|
|
121
|
+
[4] 完整包(图标+截图+简介)
|
|
122
|
+
|
|
123
|
+
请选择 (1-4): 4
|
|
124
|
+
|
|
125
|
+
✨ 下载完成!
|
|
126
|
+
📁 ~/Desktop/AppCask Downloads/X
|
|
127
|
+
📊 统计: 15 个文件, 总大小 8.45 MB
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 📖 使用示例
|
|
133
|
+
|
|
134
|
+
### 仅下载图标
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
appcask "Instagram"
|
|
138
|
+
# 选择: [1] 图标 → [3] 1024x1024
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 下载所有截图
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
appcask "王者荣耀" cn
|
|
145
|
+
# 选择: [2] 截图 → all
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 导出应用信息
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
appcask "Notion"
|
|
152
|
+
# 选择: [3] 简介信息
|
|
153
|
+
# 输出: TXT + JSON + Markdown
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 批量下载
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
#!/usr/bin/env ruby
|
|
160
|
+
|
|
161
|
+
apps = ['Instagram', 'Twitter', 'Facebook']
|
|
162
|
+
|
|
163
|
+
apps.each do |app|
|
|
164
|
+
system("appcask '#{app}'")
|
|
165
|
+
sleep 2
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 📂 文件结构
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
AppCask Downloads/
|
|
175
|
+
└── Instagram/
|
|
176
|
+
├── icons/
|
|
177
|
+
│ ├── icon-60x60.png
|
|
178
|
+
│ ├── icon-100x100.png
|
|
179
|
+
│ ├── icon-512x512.png
|
|
180
|
+
│ └── icon-1024x1024.png
|
|
181
|
+
├── screenshots/
|
|
182
|
+
│ ├── iPhone/
|
|
183
|
+
│ └── iPad/
|
|
184
|
+
├── app_info.txt
|
|
185
|
+
├── app_info.json
|
|
186
|
+
└── README.md
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 🔧 高级功能
|
|
192
|
+
|
|
193
|
+
### Debug 模式
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
DEBUG=1 appcask "AppName"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 快捷操作
|
|
200
|
+
|
|
201
|
+
- **回车** - 默认选项
|
|
202
|
+
- **q** - 退出
|
|
203
|
+
- **Ctrl+C** - 中断
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 🐛 故障排除
|
|
208
|
+
|
|
209
|
+
**Q: 搜索不到应用?**
|
|
210
|
+
A: 确认拼写、尝试切换区域
|
|
211
|
+
|
|
212
|
+
**Q: 图标尺寸不对?**
|
|
213
|
+
A: 部分应用不支持 1024x1024
|
|
214
|
+
|
|
215
|
+
**Q: 网络超时?**
|
|
216
|
+
A: 检查网络、使用 VPN
|
|
217
|
+
|
|
218
|
+
**Q: 文件保存在哪?**
|
|
219
|
+
A: `~/Desktop/AppCask Downloads/`
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🎯 路线图
|
|
224
|
+
|
|
225
|
+
- [ ] macOS App Store 支持
|
|
226
|
+
- [ ] 应用评论下载
|
|
227
|
+
- [ ] 批量下载模式
|
|
228
|
+
- [ ] Web 界面
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 📄 许可证
|
|
233
|
+
|
|
234
|
+
MIT License
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 📮 联系
|
|
239
|
+
|
|
240
|
+
- GitHub: [@yourusername](https://github.com/yourusername)
|
|
241
|
+
- Issues: [反馈问题](https://github.com/yourusername/appcask/issues)
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
<div align="center">
|
|
246
|
+
|
|
247
|
+
**Made with ❤️ and Ruby**
|
|
248
|
+
|
|
249
|
+
</div>
|
data/Rakefile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# AppCask batch download example
|
|
3
|
+
# Usage: ruby examples/batch_download.rb
|
|
4
|
+
|
|
5
|
+
require 'appcask'
|
|
6
|
+
|
|
7
|
+
# Define the list of apps to download
|
|
8
|
+
apps = [
|
|
9
|
+
{ name: 'Instagram', region: 'us', description: 'Social media app' },
|
|
10
|
+
{ name: 'Twitter', region: 'us', description: 'Microblogging platform' },
|
|
11
|
+
{ name: 'WeChat', region: 'cn', description: 'Instant messaging app' },
|
|
12
|
+
{ name: 'LINE', region: 'jp', description: 'Messaging app popular in Japan' },
|
|
13
|
+
{ name: 'KakaoTalk', region: 'kr', description: 'Messaging app popular in Korea' }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
puts '=' * 50
|
|
17
|
+
puts 'AppCask Batch Download Script'
|
|
18
|
+
puts '=' * 50
|
|
19
|
+
puts "\nPreparing to download resources for #{apps.size} apps...\n\n"
|
|
20
|
+
|
|
21
|
+
apps.each_with_index do |app, index|
|
|
22
|
+
puts "\n[#{index + 1}/#{apps.size}] Processing: #{app[:name]} (#{app[:description]})"
|
|
23
|
+
puts '-' * 50
|
|
24
|
+
|
|
25
|
+
# Set command-line arguments
|
|
26
|
+
ARGV.clear
|
|
27
|
+
ARGV << app[:name] << app[:region]
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
# Run AppCask
|
|
31
|
+
AppCask.main
|
|
32
|
+
puts "✅ #{app[:name]} downloaded successfully."
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
puts "❌ Failed to download #{app[:name]}: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Pause to avoid sending requests too frequently
|
|
38
|
+
sleep 2 unless index == apps.size - 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts "\n" + '=' * 50
|
|
42
|
+
puts 'Batch download completed!'
|
|
43
|
+
puts '=' * 50
|
data/lib/appcask.rb
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'appcask/version'
|
|
4
|
+
require 'net/https'
|
|
5
|
+
require 'open-uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
|
|
9
|
+
module AppCask
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
ICON_SIZES = {
|
|
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
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
SCREENSHOT_DEVICES = {
|
|
20
|
+
'iphone' => 'iPhone screenshots',
|
|
21
|
+
'ipad' => 'iPad screenshots'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
COUNTRIES = {
|
|
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
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
DOWNLOAD_MODES = {
|
|
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
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
def main
|
|
45
|
+
begin
|
|
46
|
+
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
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def get
|
|
58
|
+
show_banner
|
|
59
|
+
|
|
60
|
+
app_name = get_app_name
|
|
61
|
+
country = get_country
|
|
62
|
+
|
|
63
|
+
puts "\n🔍 Searching for \"#{app_name}\"..."
|
|
64
|
+
|
|
65
|
+
results = search_app(app_name, country)
|
|
66
|
+
|
|
67
|
+
if results.nil? || results['resultCount'].to_i.zero?
|
|
68
|
+
warn "❌ No apps found for \"#{app_name}\"."
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
selected_app = select_app(results)
|
|
73
|
+
return unless selected_app
|
|
74
|
+
|
|
75
|
+
mode = select_download_mode
|
|
76
|
+
return unless mode
|
|
77
|
+
|
|
78
|
+
send(mode[:method], selected_app, country)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def show_banner
|
|
84
|
+
puts <<~BANNER
|
|
85
|
+
▄▖ ▄▖ ▌
|
|
86
|
+
▌▌▛▌▛▌▌ ▀▌▛▘▙▘
|
|
87
|
+
▛▌▙▌▙▌▙▖█▌▄▌▛▖
|
|
88
|
+
▌ ▌
|
|
89
|
+
|
|
90
|
+
v#{AppCask::VERSION}
|
|
91
|
+
BANNER
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def get_app_name
|
|
95
|
+
if ARGV.count.positive?
|
|
96
|
+
ARGV[0]
|
|
97
|
+
else
|
|
98
|
+
print '📱 Enter the app name to search: '
|
|
99
|
+
$stdin.gets.chomp.strip
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def get_country
|
|
104
|
+
return ARGV[1] if ARGV.count > 1 && COUNTRIES.key?(ARGV[1])
|
|
105
|
+
|
|
106
|
+
puts "\n🌍 Select App Store region:"
|
|
107
|
+
COUNTRIES.each_slice(3) do |slice|
|
|
108
|
+
puts " " + slice.map { |code, name| "#{code.ljust(4)}- #{name}" }.join(" ")
|
|
109
|
+
end
|
|
110
|
+
print "Choose one (default: us): "
|
|
111
|
+
|
|
112
|
+
input = $stdin.gets.chomp.strip.downcase
|
|
113
|
+
input.empty? ? 'us' : (COUNTRIES.key?(input) ? input : 'us')
|
|
114
|
+
end
|
|
115
|
+
def search_app(app_name, country)
|
|
116
|
+
url = URI('https://itunes.apple.com/search')
|
|
117
|
+
params = {
|
|
118
|
+
term: app_name,
|
|
119
|
+
country: country,
|
|
120
|
+
media: 'software',
|
|
121
|
+
entity: 'software',
|
|
122
|
+
limit: '20'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
126
|
+
http.use_ssl = true
|
|
127
|
+
http.open_timeout = 10
|
|
128
|
+
http.read_timeout = 10
|
|
129
|
+
|
|
130
|
+
request = Net::HTTP::Post.new(url)
|
|
131
|
+
request.set_form_data(params)
|
|
132
|
+
|
|
133
|
+
response = http.request(request)
|
|
134
|
+
|
|
135
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
136
|
+
warn "❌ Search failed: HTTP #{response.code}"
|
|
137
|
+
return nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
json = JSON.parse(response.body)
|
|
141
|
+
|
|
142
|
+
if json['resultCount'].zero?
|
|
143
|
+
warn '😕 No apps found. Please try a different keyword.'
|
|
144
|
+
return nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
json
|
|
148
|
+
|
|
149
|
+
rescue JSON::ParserError => e
|
|
150
|
+
warn "❌ Failed to parse response: #{e.message}"
|
|
151
|
+
nil
|
|
152
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
153
|
+
warn '❌ Network timeout. Please check your connection.'
|
|
154
|
+
nil
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
warn "❌ Search error: #{e.message}"
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def select_app(results)
|
|
161
|
+
puts "\n📋 Found #{results['resultCount']} result(s):\n\n"
|
|
162
|
+
|
|
163
|
+
results['results'].each_with_index do |item, index|
|
|
164
|
+
developer = item['artistName'] || 'Unknown Developer'
|
|
165
|
+
price = item['formattedPrice'] || item['price']
|
|
166
|
+
version = item['version'] || 'N/A'
|
|
167
|
+
rating = item['averageUserRating'] ? "⭐ #{item['averageUserRating'].round(1)}" : 'No Rating'
|
|
168
|
+
|
|
169
|
+
puts " [#{index}] #{item['trackCensoredName']}"
|
|
170
|
+
puts " Developer: #{developer} | Version: #{version}"
|
|
171
|
+
puts " Price: #{price} | Rating: #{rating}"
|
|
172
|
+
puts
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
print "Select an app (0-#{results['resultCount'] - 1}, or press q to quit): "
|
|
176
|
+
input = $stdin.gets.chomp.strip
|
|
177
|
+
|
|
178
|
+
return nil if input.downcase == 'q'
|
|
179
|
+
|
|
180
|
+
index = input.to_i
|
|
181
|
+
|
|
182
|
+
unless valid_index?(index, results['resultCount'])
|
|
183
|
+
warn "❌ Invalid selection."
|
|
184
|
+
return nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
selected = results['results'][index]
|
|
188
|
+
puts "\n✅ Selected: #{selected['trackCensoredName']}"
|
|
189
|
+
selected
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def select_download_mode
|
|
193
|
+
puts "\n📦 Select download content:\n"
|
|
194
|
+
DOWNLOAD_MODES.each do |key, value|
|
|
195
|
+
puts " [#{key}] #{value[:name]}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
print "\nChoose an option (1-4): "
|
|
199
|
+
input = $stdin.gets.chomp.strip
|
|
200
|
+
|
|
201
|
+
unless DOWNLOAD_MODES.key?(input)
|
|
202
|
+
warn "❌ Invalid selection."
|
|
203
|
+
return nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
DOWNLOAD_MODES[input]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def download_icon(app_info, country)
|
|
210
|
+
puts "\n🎨 Downloading app icon\n"
|
|
211
|
+
|
|
212
|
+
size_info = select_icon_size
|
|
213
|
+
return unless size_info
|
|
214
|
+
|
|
215
|
+
artwork_url = app_info[size_info[:key]]
|
|
216
|
+
|
|
217
|
+
unless artwork_url
|
|
218
|
+
puts "❌ This app does not provide an icon at the selected size."
|
|
219
|
+
return
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# If 1024x1024 is selected, replace 512 with 1024 in the URL
|
|
223
|
+
if size_info[:display].include?('1024')
|
|
224
|
+
artwork_url = artwork_url.gsub('512x512', '1024x1024')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
puts "\n⬇️ Downloading icon..."
|
|
228
|
+
|
|
229
|
+
download_dir = create_app_directory(app_info, 'icons')
|
|
230
|
+
file_name = "icon-#{size_info[:display].split(' ').first}"
|
|
231
|
+
|
|
232
|
+
download_image(artwork_url, download_dir, file_name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def download_screenshots(app_info, country)
|
|
236
|
+
puts "\n📸 Downloading app screenshots\n"
|
|
237
|
+
|
|
238
|
+
iphone_urls = app_info['screenshotUrls'] || []
|
|
239
|
+
ipad_urls = app_info['ipadScreenshotUrls'] || []
|
|
240
|
+
|
|
241
|
+
if iphone_urls.empty? && ipad_urls.empty?
|
|
242
|
+
puts "❌ This app does not provide any screenshots."
|
|
243
|
+
return
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
puts "Available screenshots:"
|
|
247
|
+
puts " iPhone: #{iphone_urls.count}" unless iphone_urls.empty?
|
|
248
|
+
puts " iPad: #{ipad_urls.count}" unless ipad_urls.empty?
|
|
249
|
+
|
|
250
|
+
print "\nWhich device screenshots would you like to download? (iphone/ipad/all, default: all): "
|
|
251
|
+
device = $stdin.gets.chomp.strip.downcase
|
|
252
|
+
device = 'all' if device.empty?
|
|
253
|
+
|
|
254
|
+
download_dir = create_app_directory(app_info, 'screenshots')
|
|
255
|
+
|
|
256
|
+
case device
|
|
257
|
+
when 'iphone'
|
|
258
|
+
download_screenshot_set(iphone_urls, download_dir, 'iPhone')
|
|
259
|
+
when 'ipad'
|
|
260
|
+
download_screenshot_set(ipad_urls, download_dir, 'iPad')
|
|
261
|
+
else
|
|
262
|
+
download_screenshot_set(iphone_urls, download_dir, 'iPhone') unless iphone_urls.empty?
|
|
263
|
+
download_screenshot_set(ipad_urls, download_dir, 'iPad') unless ipad_urls.empty?
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def download_description(app_info, country)
|
|
268
|
+
puts "\n📝 Saving app information\n"
|
|
269
|
+
|
|
270
|
+
download_dir = create_app_directory(app_info, 'info')
|
|
271
|
+
|
|
272
|
+
# Generate detailed text information
|
|
273
|
+
info_text = generate_app_info_text(app_info)
|
|
274
|
+
|
|
275
|
+
# Generate JSON data
|
|
276
|
+
info_json = generate_app_info_json(app_info)
|
|
277
|
+
|
|
278
|
+
# Save as TXT
|
|
279
|
+
txt_file = File.join(download_dir, "app_info.txt")
|
|
280
|
+
File.open(txt_file, 'w:UTF-8') { |f| f.write(info_text) }
|
|
281
|
+
|
|
282
|
+
# Save as JSON
|
|
283
|
+
json_file = File.join(download_dir, "app_info.json")
|
|
284
|
+
File.open(json_file, 'w:UTF-8') { |f| f.write(JSON.pretty_generate(info_json)) }
|
|
285
|
+
|
|
286
|
+
# Save as Markdown
|
|
287
|
+
md_file = File.join(download_dir, "README.md")
|
|
288
|
+
md_text = generate_app_info_markdown(app_info)
|
|
289
|
+
File.open(md_file, 'w:UTF-8') { |f| f.write(md_text) }
|
|
290
|
+
|
|
291
|
+
puts "✨ App information saved successfully!"
|
|
292
|
+
puts "📁 Directory: #{download_dir}"
|
|
293
|
+
puts " - app_info.txt (Plain text)"
|
|
294
|
+
puts " - app_info.json (JSON format)"
|
|
295
|
+
puts " - README.md (Markdown format)"
|
|
296
|
+
|
|
297
|
+
# On macOS, ask whether to open the directory
|
|
298
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
299
|
+
print "\nOpen the folder now? (y/n): "
|
|
300
|
+
response = $stdin.gets.chomp.strip.downcase
|
|
301
|
+
system("open '#{download_dir}'") if response == 'y'
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def download_all(app_info, country)
|
|
306
|
+
puts "\n📦 Downloading full package (icons + screenshots + app info)\n"
|
|
307
|
+
|
|
308
|
+
base_dir = create_app_directory(app_info)
|
|
309
|
+
|
|
310
|
+
# 1. Download all icon sizes
|
|
311
|
+
puts "\n[1/3] 📥 Downloading icons..."
|
|
312
|
+
icon_dir = File.join(base_dir, 'icons')
|
|
313
|
+
FileUtils.mkdir_p(icon_dir)
|
|
314
|
+
|
|
315
|
+
ICON_SIZES.each do |_key, size_info|
|
|
316
|
+
artwork_url = app_info[size_info[:key]]
|
|
317
|
+
next unless artwork_url
|
|
318
|
+
|
|
319
|
+
url =
|
|
320
|
+
if size_info[:display].include?('1024')
|
|
321
|
+
artwork_url.gsub('512x512', '1024x1024')
|
|
322
|
+
else
|
|
323
|
+
artwork_url
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
filename = "icon-#{size_info[:display].split(' ').first}"
|
|
327
|
+
download_image(url, icon_dir, filename, silent: true)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# 2. Download all screenshots
|
|
331
|
+
puts "\n[2/3] 📥 Downloading screenshots..."
|
|
332
|
+
screenshot_dir = File.join(base_dir, 'screenshots')
|
|
333
|
+
FileUtils.mkdir_p(screenshot_dir)
|
|
334
|
+
|
|
335
|
+
iphone_urls = app_info['screenshotUrls'] || []
|
|
336
|
+
ipad_urls = app_info['ipadScreenshotUrls'] || []
|
|
337
|
+
|
|
338
|
+
download_screenshot_set(iphone_urls, screenshot_dir, 'iPhone', silent: true) unless iphone_urls.empty?
|
|
339
|
+
download_screenshot_set(ipad_urls, screenshot_dir, 'iPad', silent: true) unless ipad_urls.empty?
|
|
340
|
+
|
|
341
|
+
# 3. Save app information
|
|
342
|
+
puts "\n[3/3] 📥 Saving app information..."
|
|
343
|
+
|
|
344
|
+
info_text = generate_app_info_text(app_info)
|
|
345
|
+
info_json = generate_app_info_json(app_info)
|
|
346
|
+
info_md = generate_app_info_markdown(app_info)
|
|
347
|
+
|
|
348
|
+
File.open(File.join(base_dir, 'app_info.txt'), 'w:UTF-8') { |f| f.write(info_text) }
|
|
349
|
+
File.open(File.join(base_dir, 'app_info.json'), 'w:UTF-8') { |f| f.write(JSON.pretty_generate(info_json)) }
|
|
350
|
+
File.open(File.join(base_dir, 'README.md'), 'w:UTF-8') { |f| f.write(info_md) }
|
|
351
|
+
|
|
352
|
+
puts "\n✨ Download completed!"
|
|
353
|
+
puts "📁 All files saved to: #{base_dir}"
|
|
354
|
+
|
|
355
|
+
puts "\nDirectory structure:"
|
|
356
|
+
puts " ├── icons/ (App icons)"
|
|
357
|
+
puts " ├── screenshots/ (App screenshots)"
|
|
358
|
+
puts " ├── app_info.txt (Plain text)"
|
|
359
|
+
puts " ├── app_info.json (JSON format)"
|
|
360
|
+
puts " └── README.md (Markdown)"
|
|
361
|
+
|
|
362
|
+
# Statistics
|
|
363
|
+
files = Dir.glob(File.join(base_dir, '**', '*')).select { |f| File.file?(f) }
|
|
364
|
+
total_files = files.count
|
|
365
|
+
total_size = (files.sum { |f| File.size(f) } / 1024.0 / 1024.0).round(2)
|
|
366
|
+
|
|
367
|
+
puts "\n📊 Summary: #{total_files} files, total size #{total_size} MB"
|
|
368
|
+
|
|
369
|
+
# On macOS, ask whether to open the directory
|
|
370
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
371
|
+
print "\nOpen the folder now? (y/n): "
|
|
372
|
+
response = $stdin.gets.chomp.strip.downcase
|
|
373
|
+
system("open '#{base_dir}'") if response == 'y'
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def select_icon_size
|
|
378
|
+
puts "\n📐 Select icon size:\n"
|
|
379
|
+
ICON_SIZES.each { |key, value| puts " [#{key}] #{value[:display]}" }
|
|
380
|
+
|
|
381
|
+
print "\nSelect (0-3, default: 2): "
|
|
382
|
+
input = $stdin.gets.chomp.strip
|
|
383
|
+
input = '2' if input.empty?
|
|
384
|
+
|
|
385
|
+
unless ICON_SIZES.key?(input)
|
|
386
|
+
warn "❌ Invalid selection."
|
|
387
|
+
return nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
ICON_SIZES[input]
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def download_screenshot_set(urls, base_dir, device_name, silent: false)
|
|
394
|
+
return if urls.empty?
|
|
395
|
+
|
|
396
|
+
puts "\nDownloading #{device_name} screenshots (#{urls.count})..." unless silent
|
|
397
|
+
|
|
398
|
+
device_dir = File.join(base_dir, device_name)
|
|
399
|
+
FileUtils.mkdir_p(device_dir)
|
|
400
|
+
|
|
401
|
+
urls.each_with_index do |url, index|
|
|
402
|
+
filename = "screenshot-#{device_name}-#{index + 1}"
|
|
403
|
+
download_image(url, device_dir, filename, silent: silent)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
puts "\n✅ #{device_name} screenshots downloaded" unless silent
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def download_image(url, dir, basename, silent: false)
|
|
410
|
+
uri = URI(url)
|
|
411
|
+
|
|
412
|
+
URI.open(uri, ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE) do |image|
|
|
413
|
+
content = image.read
|
|
414
|
+
ext = detect_image_extension(content)
|
|
415
|
+
|
|
416
|
+
filename = File.join(dir, "#{basename}.#{ext}")
|
|
417
|
+
filename = get_unique_filename(filename)
|
|
418
|
+
|
|
419
|
+
File.open(filename, 'wb') { |f| f.write(content) }
|
|
420
|
+
|
|
421
|
+
unless silent
|
|
422
|
+
file_size = (File.size(filename) / 1024.0).round(2)
|
|
423
|
+
puts "\n✅ Saved: #{File.basename(filename)} (#{file_size} KB)"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
rescue StandardError => e
|
|
427
|
+
warn "❌ Download failed: #{e.message}" unless silent
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def generate_app_info_text(app_info)
|
|
431
|
+
<<~INFO
|
|
432
|
+
===================================================
|
|
433
|
+
APPLICATION DETAILS
|
|
434
|
+
===================================================
|
|
435
|
+
|
|
436
|
+
[Basic Information]
|
|
437
|
+
App Name: #{app_info['trackCensoredName']}
|
|
438
|
+
App ID: #{app_info['trackId']}
|
|
439
|
+
Bundle ID: #{app_info['bundleId']}
|
|
440
|
+
Developer: #{app_info['artistName']}
|
|
441
|
+
Developer ID: #{app_info['artistId']}
|
|
442
|
+
|
|
443
|
+
[Version Information]
|
|
444
|
+
Current Version: #{app_info['version']}
|
|
445
|
+
File Size: #{(app_info['fileSizeBytes'].to_i / 1024.0 / 1024.0).round(2)} MB
|
|
446
|
+
Minimum OS Requirement: iOS #{app_info['minimumOsVersion']}
|
|
447
|
+
Supported Devices: #{app_info['supportedDevices']&.join(', ') || 'N/A'}
|
|
448
|
+
|
|
449
|
+
[Pricing & Ratings]
|
|
450
|
+
Price: #{app_info['formattedPrice'] || app_info['price']}
|
|
451
|
+
Currency: #{app_info['currency']}
|
|
452
|
+
Rating: #{app_info['averageUserRating'] || 'N/A'} (#{app_info['userRatingCount'] || 0} ratings)
|
|
453
|
+
|
|
454
|
+
[Categories]
|
|
455
|
+
Primary Category: #{app_info['primaryGenreName']}
|
|
456
|
+
All Categories: #{app_info['genres']&.join(', ')}
|
|
457
|
+
|
|
458
|
+
[Release Information]
|
|
459
|
+
First Released: #{app_info['releaseDate']}
|
|
460
|
+
Last Updated: #{app_info['currentVersionReleaseDate']}
|
|
461
|
+
Content Rating: #{app_info['contentAdvisoryRating']}
|
|
462
|
+
|
|
463
|
+
[Description]
|
|
464
|
+
#{app_info['description']}
|
|
465
|
+
|
|
466
|
+
[Release Notes]
|
|
467
|
+
#{app_info['releaseNotes'] || 'No release notes provided.'}
|
|
468
|
+
|
|
469
|
+
[Developer Information]
|
|
470
|
+
Developer Website: #{app_info['sellerUrl']}
|
|
471
|
+
|
|
472
|
+
[Store Link]
|
|
473
|
+
App Store: #{app_info['trackViewUrl']}
|
|
474
|
+
|
|
475
|
+
===================================================
|
|
476
|
+
Exported At: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}
|
|
477
|
+
===================================================
|
|
478
|
+
INFO
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def generate_app_info_json(app_info)
|
|
482
|
+
{
|
|
483
|
+
basic: {
|
|
484
|
+
name: app_info['trackCensoredName'],
|
|
485
|
+
app_id: app_info['trackId'],
|
|
486
|
+
bundle_id: app_info['bundleId'],
|
|
487
|
+
developer: app_info['artistName'],
|
|
488
|
+
developer_id: app_info['artistId']
|
|
489
|
+
},
|
|
490
|
+
version: {
|
|
491
|
+
current_version: app_info['version'],
|
|
492
|
+
file_size_bytes: app_info['fileSizeBytes'],
|
|
493
|
+
file_size_mb: (app_info['fileSizeBytes'].to_i / 1024.0 / 1024.0).round(2),
|
|
494
|
+
minimum_os_version: app_info['minimumOsVersion'],
|
|
495
|
+
supported_devices: app_info['supportedDevices']
|
|
496
|
+
},
|
|
497
|
+
pricing: {
|
|
498
|
+
price: app_info['price'],
|
|
499
|
+
formatted_price: app_info['formattedPrice'],
|
|
500
|
+
currency: app_info['currency']
|
|
501
|
+
},
|
|
502
|
+
ratings: {
|
|
503
|
+
average_rating: app_info['averageUserRating'],
|
|
504
|
+
rating_count: app_info['userRatingCount'],
|
|
505
|
+
rating_count_current_version: app_info['userRatingCountForCurrentVersion']
|
|
506
|
+
},
|
|
507
|
+
categories: {
|
|
508
|
+
primary_genre: app_info['primaryGenreName'],
|
|
509
|
+
all_genres: app_info['genres']
|
|
510
|
+
},
|
|
511
|
+
release: {
|
|
512
|
+
release_date: app_info['releaseDate'],
|
|
513
|
+
current_version_release_date: app_info['currentVersionReleaseDate'],
|
|
514
|
+
content_rating: app_info['contentAdvisoryRating']
|
|
515
|
+
},
|
|
516
|
+
description: app_info['description'],
|
|
517
|
+
release_notes: app_info['releaseNotes'],
|
|
518
|
+
urls: {
|
|
519
|
+
app_store: app_info['trackViewUrl'],
|
|
520
|
+
developer_website: app_info['sellerUrl']
|
|
521
|
+
},
|
|
522
|
+
screenshots: {
|
|
523
|
+
iphone: app_info['screenshotUrls'],
|
|
524
|
+
ipad: app_info['ipadScreenshotUrls']
|
|
525
|
+
},
|
|
526
|
+
exported_at: Time.now.iso8601
|
|
527
|
+
}
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def generate_app_info_markdown(app_info)
|
|
531
|
+
<<~MARKDOWN
|
|
532
|
+
# #{app_info['trackCensoredName']}
|
|
533
|
+
|
|
534
|
+
> Developer: #{app_info['artistName']}
|
|
535
|
+
|
|
536
|
+

|
|
537
|
+

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