sticker-convert 2.8.12__py3-none-any.whl → 2.17.0.0__py3-none-any.whl

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.
Files changed (124) hide show
  1. sticker_convert/__main__.py +24 -24
  2. sticker_convert/auth/__init__.py +0 -0
  3. sticker_convert/auth/auth_base.py +19 -0
  4. sticker_convert/auth/auth_discord.py +149 -0
  5. sticker_convert/{utils/auth/get_kakao_auth.py → auth/auth_kakao_android_login.py} +331 -300
  6. sticker_convert/auth/auth_kakao_desktop_login.py +327 -0
  7. sticker_convert/auth/auth_kakao_desktop_memdump.py +281 -0
  8. sticker_convert/{utils/auth/get_line_auth.py → auth/auth_line.py} +98 -80
  9. sticker_convert/auth/auth_signal.py +139 -0
  10. sticker_convert/auth/auth_telethon.py +161 -0
  11. sticker_convert/auth/auth_viber.py +250 -0
  12. sticker_convert/auth/telegram_api.py +736 -0
  13. sticker_convert/cli.py +623 -509
  14. sticker_convert/converter.py +1093 -962
  15. sticker_convert/definitions.py +11 -0
  16. sticker_convert/downloaders/download_band.py +111 -0
  17. sticker_convert/downloaders/download_base.py +171 -130
  18. sticker_convert/downloaders/download_discord.py +92 -0
  19. sticker_convert/downloaders/download_kakao.py +417 -255
  20. sticker_convert/downloaders/download_line.py +484 -472
  21. sticker_convert/downloaders/download_ogq.py +80 -0
  22. sticker_convert/downloaders/download_signal.py +108 -92
  23. sticker_convert/downloaders/download_telegram.py +56 -130
  24. sticker_convert/downloaders/download_viber.py +121 -95
  25. sticker_convert/gui.py +788 -795
  26. sticker_convert/gui_components/frames/comp_frame.py +180 -165
  27. sticker_convert/gui_components/frames/config_frame.py +156 -113
  28. sticker_convert/gui_components/frames/control_frame.py +32 -30
  29. sticker_convert/gui_components/frames/cred_frame.py +232 -162
  30. sticker_convert/gui_components/frames/input_frame.py +139 -137
  31. sticker_convert/gui_components/frames/output_frame.py +112 -110
  32. sticker_convert/gui_components/frames/right_clicker.py +25 -23
  33. sticker_convert/gui_components/windows/advanced_compression_window.py +757 -715
  34. sticker_convert/gui_components/windows/base_window.py +7 -2
  35. sticker_convert/gui_components/windows/discord_get_auth_window.py +79 -0
  36. sticker_convert/gui_components/windows/kakao_get_auth_window.py +511 -186
  37. sticker_convert/gui_components/windows/line_get_auth_window.py +94 -102
  38. sticker_convert/gui_components/windows/signal_get_auth_window.py +84 -135
  39. sticker_convert/gui_components/windows/viber_get_auth_window.py +168 -0
  40. sticker_convert/ios-message-stickers-template/.github/FUNDING.yml +3 -3
  41. sticker_convert/ios-message-stickers-template/.gitignore +0 -0
  42. sticker_convert/ios-message-stickers-template/README.md +10 -10
  43. sticker_convert/ios-message-stickers-template/stickers/Info.plist +43 -43
  44. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Info.plist +31 -31
  45. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Contents.json +6 -6
  46. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Contents.json +20 -20
  47. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Contents.json +9 -9
  48. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Sticker 1.png +0 -0
  49. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Contents.json +9 -9
  50. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Sticker 2.png +0 -0
  51. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Contents.json +9 -9
  52. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Sticker 3.png +0 -0
  53. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/App-Store-1024x1024pt.png +0 -0
  54. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json +91 -91
  55. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-App-Store-1024x768pt.png +0 -0
  56. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-67x50pt@2x.png +0 -0
  57. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-Pro-74x55pt@2x.png +0 -0
  58. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@2x.png +0 -0
  59. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@3x.png +0 -0
  60. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@2x.png +0 -0
  61. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@3x.png +0 -0
  62. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@2x.png +0 -0
  63. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@3x.png +0 -0
  64. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPad-Settings-29pt@2x.png +0 -0
  65. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-Settings-29pt@3x.png +0 -0
  66. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-settings-29pt@2x.png +0 -0
  67. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.pbxproj +364 -364
  68. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -7
  69. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -8
  70. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcuserdata/niklaspeterson.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  71. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/xcuserdata/niklaspeterson.xcuserdatad/xcschemes/xcschememanagement.plist +14 -14
  72. sticker_convert/job.py +279 -179
  73. sticker_convert/job_option.py +15 -2
  74. sticker_convert/locales/en_US/LC_MESSAGES/base.mo +0 -0
  75. sticker_convert/locales/ja_JP/LC_MESSAGES/base.mo +0 -0
  76. sticker_convert/locales/zh_CN/LC_MESSAGES/base.mo +0 -0
  77. sticker_convert/locales/zh_TW/LC_MESSAGES/base.mo +0 -0
  78. sticker_convert/py.typed +0 -0
  79. sticker_convert/resources/NotoColorEmoji.ttf +0 -0
  80. sticker_convert/resources/compression.json +220 -16
  81. sticker_convert/resources/emoji.json +527 -77
  82. sticker_convert/resources/help.ja_JP.json +88 -0
  83. sticker_convert/resources/help.json +24 -10
  84. sticker_convert/resources/help.zh_CN.json +88 -0
  85. sticker_convert/resources/help.zh_TW.json +88 -0
  86. sticker_convert/resources/input.ja_JP.json +74 -0
  87. sticker_convert/resources/input.json +121 -71
  88. sticker_convert/resources/input.zh_CN.json +74 -0
  89. sticker_convert/resources/input.zh_TW.json +74 -0
  90. sticker_convert/resources/memdump_linux.sh +25 -0
  91. sticker_convert/resources/memdump_windows.ps1 +8 -0
  92. sticker_convert/resources/output.ja_JP.json +38 -0
  93. sticker_convert/resources/output.json +24 -0
  94. sticker_convert/resources/output.zh_CN.json +38 -0
  95. sticker_convert/resources/output.zh_TW.json +38 -0
  96. sticker_convert/uploaders/compress_wastickers.py +186 -156
  97. sticker_convert/uploaders/upload_base.py +44 -35
  98. sticker_convert/uploaders/upload_signal.py +218 -173
  99. sticker_convert/uploaders/upload_telegram.py +353 -388
  100. sticker_convert/uploaders/upload_viber.py +178 -0
  101. sticker_convert/uploaders/xcode_imessage.py +295 -285
  102. sticker_convert/utils/callback.py +238 -6
  103. sticker_convert/utils/chrome_remotedebug.py +219 -0
  104. sticker_convert/utils/chromiums/linux.py +52 -0
  105. sticker_convert/utils/chromiums/osx.py +68 -0
  106. sticker_convert/utils/chromiums/windows.py +45 -0
  107. sticker_convert/utils/emoji.py +28 -0
  108. sticker_convert/utils/files/json_resources_loader.py +24 -19
  109. sticker_convert/utils/files/metadata_handler.py +8 -7
  110. sticker_convert/utils/files/run_bin.py +1 -1
  111. sticker_convert/utils/media/codec_info.py +99 -67
  112. sticker_convert/utils/media/format_verify.py +33 -20
  113. sticker_convert/utils/process.py +231 -0
  114. sticker_convert/utils/translate.py +108 -0
  115. sticker_convert/utils/url_detect.py +40 -33
  116. sticker_convert/version.py +1 -1
  117. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/METADATA +189 -96
  118. sticker_convert-2.17.0.0.dist-info/RECORD +138 -0
  119. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/WHEEL +1 -1
  120. sticker_convert/utils/auth/get_signal_auth.py +0 -129
  121. sticker_convert-2.8.12.dist-info/RECORD +0 -101
  122. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/entry_points.txt +0 -0
  123. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info/licenses}/LICENSE +0 -0
  124. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/top_level.txt +0 -0
sticker_convert/job.py CHANGED
@@ -8,12 +8,16 @@ from datetime import datetime
8
8
  from multiprocessing import Manager, Process, Value
9
9
  from pathlib import Path
10
10
  from threading import Thread
11
- from typing import Any, Callable, Dict, List, Optional, Tuple
11
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union, cast
12
12
  from urllib.parse import urlparse
13
13
 
14
14
  from sticker_convert.converter import StickerConvert
15
+ from sticker_convert.definitions import RUNTIME_STATE
16
+ from sticker_convert.downloaders.download_band import DownloadBand
17
+ from sticker_convert.downloaders.download_discord import DownloadDiscord
15
18
  from sticker_convert.downloaders.download_kakao import DownloadKakao
16
19
  from sticker_convert.downloaders.download_line import DownloadLine
20
+ from sticker_convert.downloaders.download_ogq import DownloadOgq
17
21
  from sticker_convert.downloaders.download_signal import DownloadSignal
18
22
  from sticker_convert.downloaders.download_telegram import DownloadTelegram
19
23
  from sticker_convert.downloaders.download_viber import DownloadViber
@@ -21,27 +25,27 @@ from sticker_convert.job_option import CompOption, CredOption, InputOption, Outp
21
25
  from sticker_convert.uploaders.compress_wastickers import CompressWastickers
22
26
  from sticker_convert.uploaders.upload_signal import UploadSignal
23
27
  from sticker_convert.uploaders.upload_telegram import UploadTelegram
28
+ from sticker_convert.uploaders.upload_viber import UploadViber
24
29
  from sticker_convert.uploaders.xcode_imessage import XcodeImessage
25
30
  from sticker_convert.utils.callback import CallbackReturn, CbQueueType, ResultsListType, WorkQueueType
26
- from sticker_convert.utils.files.json_resources_loader import OUTPUT_JSON
31
+ from sticker_convert.utils.chrome_remotedebug import CRD
32
+ from sticker_convert.utils.files.json_resources_loader import load_resource_json
27
33
  from sticker_convert.utils.files.metadata_handler import MetadataHandler
28
34
  from sticker_convert.utils.media.codec_info import CodecInfo
35
+ from sticker_convert.utils.translate import I
36
+
37
+ if TYPE_CHECKING:
38
+ from sticker_convert.utils.callback import CallbackCli, CallbackGui
29
39
 
30
40
 
31
41
  class Executor:
32
- def __init__(
33
- self,
34
- cb_msg: Callable[..., None],
35
- cb_msg_block: Callable[..., None],
36
- cb_bar: Callable[..., None],
37
- cb_ask_bool: Callable[..., bool],
38
- cb_ask_str: Callable[..., str],
39
- ) -> None:
40
- self.cb_msg = cb_msg
41
- self.cb_msg_block = cb_msg_block
42
- self.cb_bar = cb_bar
43
- self.cb_ask_bool = cb_ask_bool
44
- self.cb_ask_str = cb_ask_str
42
+ def __init__(self, cb: Union[CallbackGui, CallbackCli]) -> None:
43
+ self.cb_msg = cb.cb_msg
44
+ self.cb_msg_block = cb.cb_msg_block
45
+ self.cb_msg_dynamic = cb.cb_msg_dynamic
46
+ self.cb_bar = cb.cb_bar
47
+ self.cb_ask_bool = cb.cb_ask_bool
48
+ self.cb_ask_str = cb.cb_ask_str
45
49
 
46
50
  self.manager = Manager()
47
51
  self.work_queue: WorkQueueType = self.manager.Queue()
@@ -85,7 +89,7 @@ class Executor:
85
89
  def cb(
86
90
  self,
87
91
  action: Optional[str],
88
- args: Optional[Tuple[str, ...]] = None,
92
+ args: Optional[Tuple[Any, ...]] = None,
89
93
  kwargs: Optional[Dict[str, Any]] = None,
90
94
  ) -> None:
91
95
  if args is None:
@@ -100,6 +104,8 @@ class Executor:
100
104
  self.cb_bar(update_bar=1)
101
105
  elif action == "msg_block":
102
106
  self.cb_return.set_response(self.cb_msg_block(*args, **kwargs))
107
+ elif action == "msg_dynamic":
108
+ self.cb_msg_dynamic(*args, **kwargs)
103
109
  elif action == "ask_bool":
104
110
  self.cb_return.set_response(self.cb_ask_bool(*args, **kwargs))
105
111
  elif action == "ask_str":
@@ -125,15 +131,24 @@ class Executor:
125
131
  arg_dump.append("CredOption(REDACTED)")
126
132
  else:
127
133
  arg_dump.append(i)
128
- e = "##### EXCEPTION #####\n"
129
- e += "Function: " + repr(work_func) + "\n"
130
- e += "Arguments: " + repr(arg_dump) + "\n"
131
- e += traceback.format_exc()
132
- e += "#####################"
134
+ e = I(
135
+ "##### EXCEPTION #####\n"
136
+ "Function: {function}\n"
137
+ "Arguments: {args}\n"
138
+ "{tb}\n"
139
+ "#####################"
140
+ ).format(
141
+ function=repr(work_func),
142
+ args=repr(arg_dump),
143
+ tb=traceback.format_exc(),
144
+ )
133
145
  cb_queue.put(e)
134
146
 
135
147
  work_queue.put(None)
136
148
  cb_queue.put("__PROCESS_DONE__")
149
+ crd = cast(CRD, RUNTIME_STATE.get("crd"))
150
+ if crd:
151
+ crd.close()
137
152
 
138
153
  def start_workers(self, processes: int = 1) -> None:
139
154
  self.cb_thread_instance = Thread(
@@ -170,6 +185,12 @@ class Executor:
170
185
  try:
171
186
  for process in self.processes:
172
187
  process.join()
188
+ if process.exitcode != 0 and self.is_cancel_job.value == 0:
189
+ self.cb_msg(
190
+ I("Warning: A process exited with error (code {})").format(
191
+ process.exitcode
192
+ )
193
+ )
173
194
  except KeyboardInterrupt:
174
195
  pass
175
196
 
@@ -198,32 +219,16 @@ class Job:
198
219
  opt_comp: CompOption,
199
220
  opt_output: OutputOption,
200
221
  opt_cred: CredOption,
201
- cb_msg: Callable[..., None],
202
- cb_msg_block: Callable[..., None],
203
- cb_bar: Callable[..., None],
204
- cb_ask_bool: Callable[..., bool],
205
- cb_ask_str: Callable[..., str],
222
+ cb: Union[CallbackCli, CallbackGui],
206
223
  ) -> None:
207
224
  self.opt_input = opt_input
208
225
  self.opt_comp = opt_comp
209
226
  self.opt_output = opt_output
210
227
  self.opt_cred = opt_cred
211
- self.cb_msg = cb_msg
212
- self.cb_msg_block = cb_msg_block
213
- self.cb_bar = cb_bar
214
- self.cb_ask_bool = cb_ask_bool
215
- self.cb_ask_str = cb_ask_str
216
228
 
217
- self.compress_fails: List[str] = []
218
229
  self.out_urls: List[str] = []
219
230
 
220
- self.executor = Executor(
221
- self.cb_msg,
222
- self.cb_msg_block,
223
- self.cb_bar,
224
- self.cb_ask_bool,
225
- self.cb_ask_str,
226
- )
231
+ self.executor = Executor(cb)
227
232
 
228
233
  def start(self) -> int:
229
234
  if Path(self.opt_input.dir).is_dir() is False:
@@ -234,28 +239,44 @@ class Job:
234
239
 
235
240
  self.executor.cb("msg", kwargs={"cls": True})
236
241
 
237
- tasks = (
242
+ tasks: Tuple[Callable[..., Tuple[bool, Optional[str]]], ...] = (
238
243
  self.verify_input,
239
244
  self.cleanup,
240
245
  self.download,
241
246
  self.compress,
242
247
  self.export,
243
- self.report,
244
248
  )
245
249
 
246
250
  code = 0
251
+ summaries: List[str] = []
247
252
  for task in tasks:
248
253
  self.executor.cb("bar", kwargs={"set_progress_mode": "indeterminate"})
249
- success = task()
254
+ success, summary = task()
255
+ if summary is not None:
256
+ summaries.append(summary)
250
257
 
251
258
  if self.executor.is_cancel_job.value == 1: # type: ignore
252
259
  code = 2
253
260
  break
254
261
  if not success:
255
262
  code = 1
256
- self.executor.cb("An error occured during this run.")
263
+ self.executor.cb(I("An error occured during this run."))
257
264
  break
258
265
 
266
+ msg = I("##########\nSummary:\n##########\n")
267
+
268
+ msg += "\n"
269
+ msg += "\n".join(summaries)
270
+ msg += "\n"
271
+
272
+ if self.out_urls:
273
+ msg += I("Export results:\n")
274
+ msg += "\n".join(self.out_urls)
275
+ else:
276
+ msg += I("Export result: None")
277
+
278
+ self.executor.cb(msg)
279
+
259
280
  self.executor.cleanup()
260
281
 
261
282
  return code
@@ -263,49 +284,63 @@ class Job:
263
284
  def cancel(self, *_: Any, **_kwargs: Any) -> None:
264
285
  self.executor.kill_workers()
265
286
 
266
- def verify_input(self) -> bool:
287
+ def verify_input(self) -> Tuple[bool, None]:
267
288
  info_msg = ""
268
289
  error_msg = ""
269
290
 
270
- save_to_local_tip = ""
271
- save_to_local_tip += " If you want to upload the results by yourself,\n"
272
- save_to_local_tip += ' select "Save to local directory only" for output\n'
291
+ save_to_local_tip = I(
292
+ "\n"
293
+ " If you want to upload the results by yourself,\n"
294
+ ' select "Save to local directory only" for output\n'
295
+ )
273
296
 
274
297
  if Path(self.opt_input.dir).resolve() == Path(self.opt_output.dir).resolve():
275
- error_msg += "\n"
276
- error_msg += "[X] Input and output directories cannot be the same\n"
298
+ error_msg += I("\n[X] Input and output directories cannot be the same\n")
277
299
 
278
300
  if self.opt_input.option == "auto":
279
- error_msg += "\n"
280
- error_msg += "[X] Unrecognized URL input source\n"
301
+ error_msg += I("\n[X] Unrecognized URL input source\n")
281
302
 
282
303
  if self.opt_input.option != "local" and not self.opt_input.url:
283
- error_msg += "\n"
284
- error_msg += "[X] URL address cannot be empty.\n"
285
- error_msg += " If you only want to use local files,\n"
286
- error_msg += ' choose "Save to local directory only"\n'
287
- error_msg += ' in "Input source"\n'
304
+ error_msg += I(
305
+ "\n"
306
+ "[X] URL address cannot be empty.\n"
307
+ " If you only want to use local files,\n"
308
+ ' choose "Save to local directory only"\n'
309
+ ' in "Input source"\n'
310
+ )
288
311
 
289
312
  if (
290
313
  self.opt_input.option == "telegram" or self.opt_output.option == "telegram"
291
314
  ) and not self.opt_cred.telegram_token:
292
- error_msg += (
315
+ error_msg += I(
293
316
  "[X] Downloading from and uploading to telegram requires bot token.\n"
294
317
  )
295
318
  error_msg += save_to_local_tip
296
319
 
320
+ if (
321
+ self.opt_input.option.startswith("discord")
322
+ and not self.opt_cred.discord_token
323
+ ):
324
+ error_msg += I("[X] Downloading from Discord requires token.\n")
325
+
297
326
  if self.opt_output.option == "telegram" and not self.opt_cred.telegram_userid:
298
- error_msg += "[X] Uploading to telegram requires user_id \n"
299
- error_msg += " (From real account, not bot account).\n"
327
+ error_msg += I(
328
+ "[X] Uploading to telegram requires user_id \n"
329
+ " (From real account, not bot account).\n"
330
+ )
300
331
  error_msg += save_to_local_tip
301
332
 
302
333
  if self.opt_output.option == "signal" and not (
303
334
  self.opt_cred.signal_uuid and self.opt_cred.signal_password
304
335
  ):
305
- error_msg += "[X] Uploading to signal requires uuid and password.\n"
336
+ error_msg += I("[X] Uploading to signal requires uuid and password.\n")
337
+ error_msg += save_to_local_tip
338
+
339
+ if self.opt_output.option == "viber" and not self.opt_cred.viber_auth:
340
+ error_msg += I("[X] Uploading to Viber requires auth data.\n")
306
341
  error_msg += save_to_local_tip
307
342
 
308
- output_presets = OUTPUT_JSON
343
+ output_presets = load_resource_json("output")
309
344
 
310
345
  input_option = self.opt_input.option
311
346
  output_option = self.opt_output.option
@@ -317,30 +352,44 @@ class Job:
317
352
  if not MetadataHandler.check_metadata_provided(
318
353
  self.opt_input.dir, input_option, metadata
319
354
  ):
320
- error_msg += f'[X] {output_presets[output_option]["full_name"]} requires {metadata}\n'
355
+ error_msg += I("[X] {output_option} requires {metadata}\n").format(
356
+ output_option=output_presets[output_option]["full_name"],
357
+ metadata=metadata,
358
+ )
321
359
  if self.opt_input.option == "local":
322
- error_msg += f" {metadata} was not supplied and {metadata}.txt is absent\n"
360
+ error_msg += I(
361
+ " {metadata} was not supplied and {metadata}.txt is absent\n"
362
+ ).format(metadata=metadata)
323
363
  else:
324
- error_msg += f" {metadata} was not supplied and input source will not provide {metadata}\n"
325
- error_msg += (
326
- f" Supply the {metadata} by filling in the option, or\n"
327
- )
328
- error_msg += f" Create {metadata}.txt with the {metadata} name\n"
364
+ error_msg += I(
365
+ " {metadata} was not supplied and input source will not provide {metadata}\n"
366
+ ).format(metadata=metadata)
367
+ error_msg += I(
368
+ " Supply the {metadata} by filling in the option, or\n"
369
+ " Create {metadata}.txt with the {metadata} name\n"
370
+ ).format(metadata=metadata)
329
371
  else:
330
- info_msg += f'[!] {output_presets[output_option]["full_name"]} requires {metadata}\n'
372
+ info_msg += I("[!] {output_option} requires {metadata}\n").format(
373
+ output_option=output_presets[output_option]["full_name"],
374
+ metadata=metadata,
375
+ )
331
376
  if self.opt_input.option == "local":
332
- info_msg += f" {metadata} was not supplied but {metadata}.txt is present\n"
333
- info_msg += f" Using {metadata} name in {metadata}.txt\n"
377
+ info_msg += I(
378
+ " {metadata} was not supplied but {metadata}.txt is present\n"
379
+ " Using {metadata} name in {metadata}.txt\n"
380
+ ).format(metadata=metadata)
334
381
  else:
335
- info_msg += f" {metadata} was not supplied but input source will provide {metadata}\n"
336
- info_msg += f" Using {metadata} provided by input source\n"
382
+ info_msg += I(
383
+ f" {metadata} was not supplied but input source will provide {metadata}\n"
384
+ f" Using {metadata} provided by input source\n"
385
+ ).format(metadata=metadata)
337
386
 
338
387
  if info_msg != "":
339
388
  self.executor.cb(info_msg)
340
389
 
341
390
  if error_msg != "":
342
391
  self.executor.cb(error_msg)
343
- return False
392
+ return False, None
344
393
 
345
394
  # Check if preset not equal to export option
346
395
  # Only warn if the compression option is available in export preset
@@ -350,17 +399,22 @@ class Job:
350
399
  not self.opt_comp.no_compress
351
400
  and self.opt_output.option != "local"
352
401
  and self.opt_comp.preset != "custom"
353
- and self.opt_output.option not in self.opt_comp.preset
402
+ and (
403
+ self.opt_output.option not in self.opt_comp.preset
404
+ and self.opt_comp.preset not in self.opt_output.option
405
+ )
354
406
  ):
355
- msg = "Compression preset does not match export option\n"
356
- msg += "You may continue, but the files will need to be compressed again before export\n"
357
- msg += "You are recommended to choose the matching option for compression and output. Continue?"
407
+ msg = I(
408
+ "Compression preset does not match export option\n"
409
+ "You may continue, but the files will need to be compressed again before export\n"
410
+ "You are recommended to choose the matching option for compression and output. Continue?"
411
+ )
358
412
 
359
413
  self.executor.cb("ask_bool", (msg,))
360
414
  response = self.executor.cb_return.get_response()
361
415
 
362
416
  if response is False:
363
- return False
417
+ return False, None
364
418
 
365
419
  for param, value in (
366
420
  ("fps_power", self.opt_comp.fps_power),
@@ -369,8 +423,10 @@ class Job:
369
423
  ("color_power", self.opt_comp.color_power),
370
424
  ):
371
425
  if value < -1:
372
- error_msg += "\n"
373
- error_msg += f"[X] {param} should be between -1 and positive infinity. {value} was given."
426
+ error_msg += I(
427
+ "\n"
428
+ "[X] {param} should be between -1 and positive infinity. {value} was given."
429
+ ).format(param=param, value=value)
374
430
 
375
431
  if self.opt_comp.scale_filter not in (
376
432
  "nearest",
@@ -380,27 +436,32 @@ class Job:
380
436
  "bicubic",
381
437
  "lanczos",
382
438
  ):
383
- error_msg += "\n"
384
- error_msg += (
385
- f"[X] scale_filter {self.opt_comp.scale_filter} is not valid option"
386
- )
387
- error_msg += (
439
+ error_msg += I(
440
+ "\n"
441
+ "[X] scale_filter {scale_filter} is not valid option\n"
388
442
  " Valid options: nearest, box, bilinear, hamming, bicubic, lanczos"
389
- )
390
-
391
- if self.opt_comp.quantize_method not in ("imagequant", "fastoctree", "none"):
392
- error_msg += "\n"
393
- error_msg += f"[X] quantize_method {self.opt_comp.quantize_method} is not valid option"
394
- error_msg += " Valid options: imagequant, fastoctree, none"
443
+ ).format(scale_filter=self.opt_comp.scale_filter)
444
+
445
+ if self.opt_comp.quantize_method not in (
446
+ "imagequant",
447
+ "fastoctree",
448
+ "maxcoverage",
449
+ "mediancut",
450
+ "none",
451
+ ):
452
+ I(
453
+ "\n"
454
+ "[X] quantize_method {quantize_method} is not valid option\n"
455
+ " Valid options: imagequant, fastoctree, maxcoverage, mediancut, none"
456
+ ).format(quantize_method=self.opt_comp.quantize_method)
395
457
 
396
458
  if self.opt_comp.bg_color:
397
459
  try:
398
460
  _, _, _ = bytes.fromhex(self.opt_comp.bg_color)
399
461
  except ValueError:
400
- error_msg += "\n"
401
- error_msg += (
402
- f"[X] bg_color {self.opt_comp.bg_color} is not valid color hex"
403
- )
462
+ error_msg += I(
463
+ "\n[X] bg_color {bg_color} is not valid color hex"
464
+ ).format(bg_color=self.opt_comp.bg_color)
404
465
 
405
466
  # Warn about unable to download animated Kakao stickers with such link
406
467
  if (
@@ -408,18 +469,20 @@ class Job:
408
469
  and urlparse(self.opt_input.url).netloc == "e.kakao.com"
409
470
  and not self.opt_cred.kakao_auth_token
410
471
  ):
411
- msg = "To download ANIMATED stickers from e.kakao.com,\n"
412
- msg += "you need to generate auth_token.\n"
413
- msg += "Alternatively, you can generate share link (emoticon.kakao.com/items/xxxxx)\n"
414
- msg += "from Kakao app on phone.\n"
415
- msg += "You are adviced to read documentations.\n"
416
- msg += "If you continue, you will only download static stickers. Continue?"
472
+ msg = I(
473
+ "To download ANIMATED stickers from e.kakao.com,\n"
474
+ "you need to generate auth_token.\n"
475
+ "Alternatively, you can generate share link (emoticon.kakao.com/items/xxxxx)\n"
476
+ "from Kakao app on phone.\n"
477
+ "You are adviced to read documentations.\n"
478
+ "If you continue, you will only download static stickers. Continue?"
479
+ )
417
480
 
418
481
  self.executor.cb("ask_bool", (msg,))
419
482
  response = self.executor.cb_return.get_response()
420
483
 
421
484
  if response is False:
422
- return False
485
+ return False, None
423
486
 
424
487
  # Warn about in/output directories that might contain other files
425
488
  # Directory is safe if the name is stickers_input/stickers_output, or
@@ -443,26 +506,33 @@ class Job:
443
506
 
444
507
  related_files = MetadataHandler.get_files_related_to_sticker_convert(path)
445
508
  if any(i for i in path.iterdir() if i not in related_files):
446
- msg = "WARNING: {} directory is set to {}.\n"
447
- msg += 'It does not have default name of "{}",\n'
448
- msg += "and It seems like it contains PERSONAL DATA.\n"
449
- msg += "During execution, contents of this directory\n"
450
- msg += 'maybe MOVED to "archive_*".\n'
451
- msg += "THIS MAY CAUSE DAMAGE TO YOUR DATA. Continue?"
509
+ msg = I(
510
+ "WARNING: {path_type} directory is set to {path}.\n"
511
+ 'It does not have default name of "{default_name}",\n'
512
+ "and It seems like it contains PERSONAL DATA.\n"
513
+ "During execution, contents of this directory\n"
514
+ 'maybe MOVED to "archive_*".\n'
515
+ "THIS MAY CAUSE DAMAGE TO YOUR DATA. Continue?"
516
+ )
452
517
 
453
518
  self.executor.cb(
454
- "ask_bool", (msg.format(path_type, path, default_name),)
519
+ "ask_bool",
520
+ (
521
+ msg.format(
522
+ path_type=path_type, path=path, default_name=default_name
523
+ ),
524
+ ),
455
525
  )
456
526
  response = self.executor.cb_return.get_response()
457
527
 
458
528
  if response is False:
459
- return False
529
+ return False, None
460
530
 
461
531
  break
462
532
 
463
- return True
533
+ return True, None
464
534
 
465
- def cleanup(self) -> bool:
535
+ def cleanup(self) -> Tuple[bool, None]:
466
536
  # If input is 'From local directory', then we should keep files in input/output directory as it maybe edited by user
467
537
  # If input is not 'From local directory', then we should move files in input/output directory as new files will be downloaded
468
538
  # Output directory should be cleanup unless no_compress is true (meaning files in output directory might be edited by user)
@@ -479,16 +549,18 @@ class Job:
479
549
 
480
550
  if self.opt_input.option == "local":
481
551
  self.executor.cb(
482
- "Skip moving old files in input directory as input source is local"
552
+ I("Skip moving old files in input directory as input source is local")
483
553
  )
484
554
  elif len(in_dir_files) == 0:
485
555
  self.executor.cb(
486
- "Skip moving old files in input directory as input source is empty"
556
+ I("Skip moving old files in input directory as input source is empty")
487
557
  )
488
558
  else:
489
559
  archive_dir = Path(self.opt_input.dir, dir_name)
490
560
  self.executor.cb(
491
- f"Moving old files in input directory to {archive_dir} as input source is not local"
561
+ I(
562
+ "Moving old files in input directory to {archive_dir} as input source is not local"
563
+ ).format(archive_dir=archive_dir)
492
564
  )
493
565
  archive_dir.mkdir(exist_ok=True)
494
566
  for old_path in in_dir_files:
@@ -497,24 +569,28 @@ class Job:
497
569
 
498
570
  if self.opt_comp.no_compress:
499
571
  self.executor.cb(
500
- "Skip moving old files in output directory as no_compress is True"
572
+ I("Skip moving old files in output directory as no_compress is True")
501
573
  )
502
574
  elif len(out_dir_files) == 0:
503
575
  self.executor.cb(
504
- "Skip moving old files in output directory as output source is empty"
576
+ I("Skip moving old files in output directory as output source is empty")
505
577
  )
506
578
  else:
507
579
  archive_dir = Path(self.opt_output.dir, dir_name)
508
- self.executor.cb(f"Moving old files in output directory to {archive_dir}")
580
+ self.executor.cb(
581
+ I("Moving old files in output directory to {archive_dir}").format(
582
+ archive_dir=archive_dir
583
+ )
584
+ )
509
585
  os.makedirs(archive_dir)
510
586
  for old_path in out_dir_files:
511
587
  new_path = Path(archive_dir, old_path.name)
512
588
  old_path.rename(new_path)
513
589
 
514
- return True
590
+ return True, None
515
591
 
516
- def download(self) -> bool:
517
- downloaders: List[Callable[..., bool]] = []
592
+ def download(self) -> Tuple[bool, str]:
593
+ downloaders: List[Callable[..., Tuple[int, int]]] = []
518
594
 
519
595
  if self.opt_input.option == "signal":
520
596
  downloaders.append(DownloadSignal.start)
@@ -522,41 +598,59 @@ class Job:
522
598
  if self.opt_input.option == "line":
523
599
  downloaders.append(DownloadLine.start)
524
600
 
525
- if self.opt_input.option == "telegram":
601
+ if self.opt_input.option.startswith("telegram"):
526
602
  downloaders.append(DownloadTelegram.start)
527
603
 
528
604
  if self.opt_input.option == "kakao":
529
605
  downloaders.append(DownloadKakao.start)
530
606
 
607
+ if self.opt_input.option == "band":
608
+ downloaders.append(DownloadBand.start)
609
+
610
+ if self.opt_input.option == "ogq":
611
+ downloaders.append(DownloadOgq.start)
612
+
531
613
  if self.opt_input.option == "viber":
532
614
  downloaders.append(DownloadViber.start)
533
615
 
616
+ if self.opt_input.option.startswith("discord"):
617
+ downloaders.append(DownloadDiscord.start)
618
+
534
619
  if len(downloaders) > 0:
535
- self.executor.cb("Downloading...")
620
+ self.executor.cb(I("Downloading..."))
536
621
  else:
537
- self.executor.cb("Nothing to download")
538
- return True
622
+ self.executor.cb(I("Skipped download (No files to download)"))
623
+ return True, I("Download: Skipped (No files to download)")
539
624
 
540
625
  self.executor.start_workers(processes=1)
541
626
 
542
627
  for downloader in downloaders:
543
628
  self.executor.add_work(
544
629
  work_func=downloader,
545
- work_args=(self.opt_input.url, self.opt_input.dir, self.opt_cred),
630
+ work_args=(self.opt_input, self.opt_cred),
546
631
  )
547
632
 
548
633
  self.executor.join_workers()
549
634
 
550
635
  # Return False if any of the job returns failure
636
+ stickers_ok = 0
637
+ stickers_total = 0
638
+ success = True
551
639
  for result in self.executor.results_list:
552
- if result is False:
553
- return False
640
+ stickers_ok += result[0]
641
+ stickers_total += result[1]
642
+ success = (
643
+ success if stickers_ok == stickers_total and stickers_ok > 0 else False
644
+ )
554
645
 
555
- return True
646
+ return (
647
+ success,
648
+ f"Download: {stickers_ok}/{stickers_total} stickers success",
649
+ )
556
650
 
557
- def compress(self) -> bool:
651
+ def compress(self) -> Tuple[bool, str]:
558
652
  if self.opt_comp.no_compress is True:
559
- self.executor.cb("no_compress is set to True, skip compression")
653
+ self.executor.cb(I("Skipped compression (no_compress is set to True)"))
560
654
  in_dir_files = [
561
655
  i
562
656
  for i in sorted(self.opt_input.dir.iterdir())
@@ -569,21 +663,23 @@ class Job:
569
663
  ]
570
664
  if len(in_dir_files) == 0:
571
665
  self.executor.cb(
572
- "Input directory is empty, nothing to copy to output directory"
666
+ I("Input directory is empty, nothing to copy to output directory")
573
667
  )
574
668
  elif len(out_dir_files) != 0:
575
669
  self.executor.cb(
576
- "Output directory is not empty, not copying files from input directory"
670
+ I(
671
+ "Output directory is not empty, not copying files from input directory"
672
+ )
577
673
  )
578
674
  else:
579
675
  self.executor.cb(
580
- "Output directory is empty, copying files from input directory"
676
+ I("Output directory is empty, copying files from input directory")
581
677
  )
582
678
  for i in in_dir_files:
583
679
  src_f = Path(self.opt_input.dir, i.name)
584
680
  dst_f = Path(self.opt_output.dir, i.name)
585
681
  shutil.copy(src_f, dst_f)
586
- return True
682
+ return True, "Compress: Skipped (no_compress is set to True)"
587
683
  msg = "Compressing..."
588
684
 
589
685
  input_dir = Path(self.opt_input.dir)
@@ -598,15 +694,20 @@ class Job:
598
694
 
599
695
  if not in_f.is_file():
600
696
  continue
601
- if CodecInfo.get_file_ext(i) in (".txt", ".m4a") or Path(i).stem == "cover":
697
+ if (
698
+ CodecInfo.get_file_ext(i) in (".txt", ".m4a")
699
+ or (
700
+ self.opt_comp.preset != "signal" and Path(i).stem == "cover"
701
+ ) # Signal cover has same spec as normal sticker
702
+ ):
602
703
  shutil.copy(in_f, output_dir / i.name)
603
704
  else:
604
705
  in_fs.append(i)
605
706
 
606
707
  in_fs_count = len(in_fs)
607
708
  if in_fs_count == 0:
608
- self.executor.cb("No files to compress")
609
- return True
709
+ self.executor.cb(I("Skipped compression (No files to compress)"))
710
+ return True, "Compress: Skipped (No files to compress)"
610
711
 
611
712
  self.executor.cb(msg)
612
713
  self.executor.cb(
@@ -625,21 +726,35 @@ class Job:
625
726
 
626
727
  self.executor.join_workers()
627
728
 
628
- # Return False if any of the job returns failure
729
+ success = True
730
+ stickers_ok = 0
731
+ stickers_total = 0
732
+ fails: List[str] = []
629
733
  for result in self.executor.results_list:
734
+ stickers_total += 1
630
735
  if result[0] is False:
631
- return False
736
+ success = False
737
+ fails.append(str(result[1]))
738
+ else:
739
+ stickers_ok += 1
632
740
 
633
- return True
741
+ msg_append = ""
742
+ if success is False:
743
+ msg_append = " (Failed: " + ", ".join(fails) + ")"
634
744
 
635
- def export(self) -> bool:
745
+ return (
746
+ success,
747
+ f"Compress: {stickers_ok}/{stickers_total} stickers success" + msg_append,
748
+ )
749
+
750
+ def export(self) -> Tuple[bool, str]:
636
751
  if self.opt_output.option == "local":
637
- self.executor.cb("Saving to local directory only, nothing to export")
638
- return True
752
+ self.executor.cb(I("Skipped export (Saving to local directory only)"))
753
+ return True, I("Export: Skipped (Saving to local directory only)")
639
754
 
640
- self.executor.cb("Exporting...")
755
+ self.executor.cb(I("Exporting..."))
641
756
 
642
- exporters: List[Callable[..., List[str]]] = []
757
+ exporters: List[Callable[..., Tuple[int, int, List[str]]]] = []
643
758
 
644
759
  if self.opt_output.option == "whatsapp":
645
760
  exporters.append(CompressWastickers.start)
@@ -647,15 +762,15 @@ class Job:
647
762
  if self.opt_output.option == "signal":
648
763
  exporters.append(UploadSignal.start)
649
764
 
650
- if self.opt_output.option == "telegram":
651
- exporters.append(UploadTelegram.start)
652
-
653
- if self.opt_output.option == "telegram_emoji":
765
+ if self.opt_output.option.startswith("telegram"):
654
766
  exporters.append(UploadTelegram.start)
655
767
 
656
768
  if self.opt_output.option == "imessage":
657
769
  exporters.append(XcodeImessage.start)
658
770
 
771
+ if self.opt_output.option == "viber":
772
+ exporters.append(UploadViber.start)
773
+
659
774
  self.executor.start_workers(processes=1)
660
775
 
661
776
  for exporter in exporters:
@@ -666,8 +781,12 @@ class Job:
666
781
 
667
782
  self.executor.join_workers()
668
783
 
784
+ stickers_ok = 0
785
+ stickers_total = 0
669
786
  for result in self.executor.results_list:
670
- self.out_urls.extend(result)
787
+ stickers_ok += result[0]
788
+ stickers_total += result[1]
789
+ self.out_urls.extend(result[2])
671
790
 
672
791
  if self.out_urls:
673
792
  with open(
@@ -675,30 +794,11 @@ class Job:
675
794
  ) as f:
676
795
  f.write("\n".join(self.out_urls))
677
796
  else:
678
- self.executor.cb("An error occured while exporting stickers")
679
- return False
680
-
681
- return True
682
-
683
- def report(self) -> bool:
684
- msg = "##########\n"
685
- msg += "Summary:\n"
686
- msg += "##########\n"
687
- msg += "\n"
688
-
689
- if self.compress_fails:
690
- msg += f'Warning: Could not compress the following {len(self.compress_fails)} file{"s" if len(self.compress_fails) > 1 else ""}:\n'
691
- msg += "\n".join(self.compress_fails)
692
- msg += "\n"
693
- msg += "\nConsider adjusting compression parameters"
694
- msg += "\n"
695
-
696
- if self.out_urls:
697
- msg += "Export results:\n"
698
- msg += "\n".join(self.out_urls)
699
- else:
700
- msg += "Export result: None"
701
-
702
- self.executor.cb(msg)
703
-
704
- return True
797
+ self.executor.cb(I("An error occured while exporting stickers"))
798
+ return False, I(
799
+ "Export: {stickers_ok}/{stickers_total} stickers success"
800
+ ).format(stickers_ok=stickers_ok, stickers_total=stickers_total)
801
+
802
+ return True, I(
803
+ "Export: {stickers_ok}/{stickers_total} stickers success"
804
+ ).format(stickers_ok=stickers_ok, stickers_total=stickers_total)