sticker-convert 2.1.6__py3-none-any.whl → 2.1.7__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 (57) hide show
  1. sticker_convert/__init__.py +1 -1
  2. sticker_convert/__main__.py +7 -4
  3. sticker_convert/cli.py +39 -31
  4. sticker_convert/converter.py +432 -0
  5. sticker_convert/downloaders/download_base.py +40 -16
  6. sticker_convert/downloaders/download_kakao.py +103 -136
  7. sticker_convert/downloaders/download_line.py +16 -6
  8. sticker_convert/downloaders/download_signal.py +48 -32
  9. sticker_convert/downloaders/download_telegram.py +71 -26
  10. sticker_convert/gui.py +78 -129
  11. sticker_convert/{gui_frames → gui_components/frames}/comp_frame.py +2 -3
  12. sticker_convert/{gui_frames → gui_components/frames}/config_frame.py +3 -4
  13. sticker_convert/{gui_frames → gui_components/frames}/control_frame.py +2 -2
  14. sticker_convert/{gui_frames → gui_components/frames}/cred_frame.py +4 -4
  15. sticker_convert/{gui_frames → gui_components/frames}/input_frame.py +4 -4
  16. sticker_convert/{gui_frames → gui_components/frames}/output_frame.py +3 -3
  17. sticker_convert/{gui_frames → gui_components/frames}/progress_frame.py +1 -1
  18. sticker_convert/{utils → gui_components}/gui_utils.py +38 -21
  19. sticker_convert/{gui_windows → gui_components/windows}/advanced_compression_window.py +3 -2
  20. sticker_convert/{gui_windows → gui_components/windows}/base_window.py +3 -2
  21. sticker_convert/{gui_windows → gui_components/windows}/kakao_get_auth_window.py +3 -3
  22. sticker_convert/{gui_windows → gui_components/windows}/line_get_auth_window.py +2 -2
  23. sticker_convert/{gui_windows → gui_components/windows}/signal_get_auth_window.py +2 -2
  24. sticker_convert/{flow.py → job.py} +91 -102
  25. sticker_convert/job_option.py +301 -0
  26. sticker_convert/resources/compression.json +1 -1
  27. sticker_convert/uploaders/compress_wastickers.py +95 -74
  28. sticker_convert/uploaders/upload_base.py +16 -4
  29. sticker_convert/uploaders/upload_signal.py +100 -62
  30. sticker_convert/uploaders/upload_telegram.py +168 -128
  31. sticker_convert/uploaders/xcode_imessage.py +202 -132
  32. sticker_convert/{auth → utils/auth}/get_kakao_auth.py +7 -5
  33. sticker_convert/{auth → utils/auth}/get_line_auth.py +3 -3
  34. sticker_convert/{auth → utils/auth}/get_signal_auth.py +1 -1
  35. sticker_convert/utils/fake_cb_msg.py +5 -2
  36. sticker_convert/utils/{cache_store.py → files/cache_store.py} +7 -3
  37. sticker_convert/utils/files/dir_utils.py +64 -0
  38. sticker_convert/utils/{json_manager.py → files/json_manager.py} +5 -4
  39. sticker_convert/utils/files/metadata_handler.py +226 -0
  40. sticker_convert/utils/files/run_bin.py +58 -0
  41. sticker_convert/utils/{apple_png_normalize.py → media/apple_png_normalize.py} +23 -20
  42. sticker_convert/utils/{codec_info.py → media/codec_info.py} +41 -35
  43. sticker_convert/utils/media/decrypt_kakao.py +68 -0
  44. sticker_convert/utils/media/format_verify.py +184 -0
  45. sticker_convert/utils/url_detect.py +16 -14
  46. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/METADATA +8 -9
  47. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/RECORD +52 -50
  48. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/WHEEL +1 -1
  49. sticker_convert/utils/converter.py +0 -407
  50. sticker_convert/utils/curr_dir.py +0 -70
  51. sticker_convert/utils/format_verify.py +0 -188
  52. sticker_convert/utils/metadata_handler.py +0 -190
  53. sticker_convert/utils/run_bin.py +0 -46
  54. /sticker_convert/{gui_frames → gui_components/frames}/right_clicker.py +0 -0
  55. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/LICENSE +0 -0
  56. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/entry_points.txt +0 -0
  57. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/top_level.txt +0 -0
@@ -1,47 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
1
3
  import platform
2
4
  from typing import Union, TYPE_CHECKING
3
5
 
4
6
  from ttkbootstrap import Frame, Canvas, Scrollbar, PhotoImage
7
+
5
8
  if TYPE_CHECKING:
6
- from ..gui_windows.base_window import BaseWindow # type: ignore
7
- from ..gui import GUI # type: ignore
9
+ from .windows.base_window import BaseWindow # type: ignore
10
+ from ..gui import GUI # type: ignore
11
+
8
12
 
9
13
  class GUIUtils:
10
14
  @staticmethod
11
15
  def set_icon(window: Union["BaseWindow", "GUI"]):
12
- window.icon = PhotoImage(file='resources/appicon.png')
16
+ window.icon = PhotoImage(file="resources/appicon.png")
13
17
  window.iconphoto(1, window.icon)
14
- if platform.system() == 'Darwin':
15
- window.iconbitmap(bitmap='resources/appicon.icns')
16
- elif platform.system() == 'Windows':
17
- window.iconbitmap(bitmap='resources/appicon.ico')
18
- window.tk.call('wm', 'iconphoto', window._w, window.icon)
18
+ if platform.system() == "Darwin":
19
+ window.iconbitmap(bitmap="resources/appicon.icns")
20
+ elif platform.system() == "Windows":
21
+ window.iconbitmap(bitmap="resources/appicon.ico")
22
+ window.tk.call("wm", "iconphoto", window._w, window.icon)
19
23
 
20
24
  @staticmethod
21
- def create_scrollable_frame(window: Union["BaseWindow", "GUI"]) -> tuple[Frame, Frame, Canvas, Scrollbar, Scrollbar, Frame]:
25
+ def create_scrollable_frame(
26
+ window: Union["BaseWindow", "GUI"]
27
+ ) -> tuple[Frame, Frame, Canvas, Scrollbar, Scrollbar, Frame]:
22
28
  main_frame = Frame(window)
23
- main_frame.pack(fill='both', expand=1)
29
+ main_frame.pack(fill="both", expand=1)
24
30
 
25
31
  horizontal_scrollbar_frame = Frame(main_frame)
26
- horizontal_scrollbar_frame.pack(fill='x', side='bottom')
32
+ horizontal_scrollbar_frame.pack(fill="x", side="bottom")
27
33
 
28
34
  canvas = Canvas(main_frame)
29
- canvas.pack(side='left', fill='both', expand=1)
35
+ canvas.pack(side="left", fill="both", expand=1)
30
36
 
31
- x_scrollbar = Scrollbar(horizontal_scrollbar_frame, orient='horizontal', command=canvas.xview)
32
- x_scrollbar.pack(side='bottom', fill='x')
37
+ x_scrollbar = Scrollbar(
38
+ horizontal_scrollbar_frame, orient="horizontal", command=canvas.xview
39
+ )
40
+ x_scrollbar.pack(side="bottom", fill="x")
33
41
 
34
- y_scrollbar = Scrollbar(main_frame, orient='vertical', command=canvas.yview)
35
- y_scrollbar.pack(side='right', fill='y')
42
+ y_scrollbar = Scrollbar(main_frame, orient="vertical", command=canvas.yview)
43
+ y_scrollbar.pack(side="right", fill="y")
36
44
 
37
45
  canvas.configure(xscrollcommand=x_scrollbar.set)
38
46
  canvas.configure(yscrollcommand=y_scrollbar.set)
39
- canvas.bind("<Configure>",lambda e: canvas.config(scrollregion=canvas.bbox('all')))
47
+ canvas.bind(
48
+ "<Configure>", lambda e: canvas.config(scrollregion=canvas.bbox("all"))
49
+ )
40
50
 
41
51
  scrollable_frame = Frame(canvas)
42
- canvas.create_window((0,0), window=scrollable_frame, anchor="nw")
52
+ canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
43
53
 
44
- return main_frame, horizontal_scrollbar_frame, canvas, x_scrollbar, y_scrollbar, scrollable_frame
54
+ return (
55
+ main_frame,
56
+ horizontal_scrollbar_frame,
57
+ canvas,
58
+ x_scrollbar,
59
+ y_scrollbar,
60
+ scrollable_frame,
61
+ )
45
62
 
46
63
  @staticmethod
47
64
  def finalize_window(window: Union["GUI", "BaseWindow"]):
@@ -62,7 +79,7 @@ class GUIUtils:
62
79
  window_width = screen_width
63
80
  if window_height > screen_height:
64
81
  window_height = screen_height
65
-
82
+
66
83
  frame_width = window_width - window.y_scrollbar.winfo_width()
67
84
  frame_height = window_height - window.x_scrollbar.winfo_height()
68
85
 
@@ -73,4 +90,4 @@ class GUIUtils:
73
90
 
74
91
  window.attributes("-alpha", 1)
75
92
 
76
- window.focus_force()
93
+ window.focus_force()
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  from functools import partial
3
4
 
4
5
  from PIL import Image, ImageTk, ImageDraw
5
6
  from ttkbootstrap import LabelFrame, Frame, OptionMenu, Button, Entry, Label, Checkbutton, Scrollbar, Canvas, StringVar # type: ignore
6
7
  from tkinter import Event
7
8
 
8
- from ..gui_frames.right_clicker import RightClicker # type: ignore
9
+ from ..frames.right_clicker import RightClicker # type: ignore
9
10
  from .base_window import BaseWindow # type: ignore
10
- from ..utils.gui_utils import GUIUtils # type: ignore
11
+ from ..gui_utils import GUIUtils # type: ignore
11
12
 
12
13
  class AdvancedCompressionWindow(BaseWindow):
13
14
  emoji_column_per_row = 10
@@ -1,11 +1,12 @@
1
+ #!/usr/bin/env python3
1
2
  import platform
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  from ttkbootstrap import Toplevel # type: ignore
5
6
 
6
7
  if TYPE_CHECKING:
7
- from ..gui import GUI # type: ignore
8
- from ..utils.gui_utils import GUIUtils # type: ignore
8
+ from ...gui import GUI # type: ignore
9
+ from ..gui_utils import GUIUtils # type: ignore
9
10
 
10
11
  class BaseWindow(Toplevel):
11
12
  def __init__(self, gui: "GUI"):
@@ -4,10 +4,10 @@ from threading import Thread
4
4
 
5
5
  from ttkbootstrap import LabelFrame, Frame, Button, Entry, Label # type: ignore
6
6
 
7
- from ..auth.get_kakao_auth import GetKakaoAuth # type: ignore
8
- from ..gui_frames.right_clicker import RightClicker # type: ignore
7
+ from ...utils.auth.get_kakao_auth import GetKakaoAuth # type: ignore
8
+ from ..frames.right_clicker import RightClicker # type: ignore
9
9
  from .base_window import BaseWindow # type: ignore
10
- from ..utils.gui_utils import GUIUtils # type: ignore
10
+ from ..gui_utils import GUIUtils # type: ignore
11
11
 
12
12
  class KakaoGetAuthWindow(BaseWindow):
13
13
  def __init__(self, *args, **kwargs):
@@ -5,9 +5,9 @@ from threading import Thread
5
5
 
6
6
  from ttkbootstrap import Frame, Button, Label # type: ignore
7
7
 
8
- from ..auth.get_line_auth import GetLineAuth # type: ignore
8
+ from ...utils.auth.get_line_auth import GetLineAuth # type: ignore
9
9
  from .base_window import BaseWindow # type: ignore
10
- from ..utils.gui_utils import GUIUtils # type: ignore
10
+ from ..gui_utils import GUIUtils # type: ignore
11
11
 
12
12
  class LineGetAuthWindow(BaseWindow):
13
13
  def __init__(self, *args, **kwargs):
@@ -4,9 +4,9 @@ from threading import Thread
4
4
 
5
5
  from ttkbootstrap import Toplevel, Frame, Button, Label # type: ignore
6
6
 
7
- from ..auth.get_signal_auth import GetSignalAuth # type: ignore
7
+ from ...utils.auth.get_signal_auth import GetSignalAuth # type: ignore
8
8
  from .base_window import BaseWindow # type: ignore
9
- from ..utils.gui_utils import GUIUtils # type: ignore
9
+ from ..gui_utils import GUIUtils # type: ignore
10
10
 
11
11
  class SignalGetAuthWindow(BaseWindow):
12
12
  def __init__(self, *args, **kwargs):
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
2
4
  import os
3
5
  import shutil
4
6
  from datetime import datetime
5
7
  from multiprocessing import Process, Queue, Value
8
+ from multiprocessing.queues import Queue as QueueType
6
9
  from threading import Thread
7
10
  from urllib.parse import urlparse
8
11
  from typing import Optional
9
12
 
13
+ from .job_option import InputOption, CompOption, OutputOption, CredOption # type: ignore
14
+
10
15
  from .downloaders.download_line import DownloadLine # type: ignore
11
16
  from .downloaders.download_signal import DownloadSignal # type: ignore
12
17
  from .downloaders.download_telegram import DownloadTelegram # type: ignore
@@ -18,22 +23,21 @@ from .uploaders.upload_telegram import UploadTelegram # type: ignore
18
23
  from .uploaders.compress_wastickers import CompressWastickers # type: ignore
19
24
  from .uploaders.xcode_imessage import XcodeImessage # type: ignore
20
25
 
21
- from .utils.converter import StickerConvert # type: ignore
22
- from .utils.codec_info import CodecInfo # type: ignore
23
- from .utils.json_manager import JsonManager # type: ignore
24
- from .utils.metadata_handler import MetadataHandler # type: ignore
26
+ from .converter import StickerConvert # type: ignore
27
+ from .utils.media.codec_info import CodecInfo # type: ignore
28
+ from .utils.files.json_manager import JsonManager # type: ignore
29
+ from .utils.files.metadata_handler import MetadataHandler # type: ignore
25
30
 
26
- class Flow:
31
+ class Job:
27
32
  def __init__(self,
28
- opt_input: dict, opt_comp: dict, opt_output: dict, opt_cred: dict,
29
- input_presets: dict, output_presets: dict, cb_msg, cb_msg_block, cb_bar, cb_ask_bool):
33
+ opt_input: InputOption, opt_comp: CompOption,
34
+ opt_output: OutputOption, opt_cred: CredOption,
35
+ cb_msg, cb_msg_block, cb_bar, cb_ask_bool):
30
36
 
31
37
  self.opt_input = opt_input
32
38
  self.opt_comp = opt_comp
33
39
  self.opt_output = opt_output
34
40
  self.opt_cred = opt_cred
35
- self.input_presets = input_presets
36
- self.output_presets = output_presets
37
41
  self.cb_msg = cb_msg
38
42
  self.cb_msg_block = cb_msg_block
39
43
  self.cb_bar = cb_bar
@@ -42,25 +46,24 @@ class Flow:
42
46
  self.compress_fails: list[str] = []
43
47
  self.out_urls: list[str] = []
44
48
 
45
- self.jobs_queue: Queue[Optional[tuple[str, str, dict]]] = Queue()
46
- self.results_queue: Queue[Optional[tuple[bool, str, str, int]]] = Queue()
47
- self.cb_msg_queue: Queue[Optional[str]] = Queue()
49
+ self.jobs_queue: QueueType[Optional[tuple[str, str, CompOption]]] = Queue()
50
+ self.results_queue: QueueType[Optional[tuple[bool, str, str, int]]] = Queue()
51
+ self.cb_msg_queue: QueueType[Optional[str]] = Queue()
48
52
  self.processes: list[Process] = []
49
53
 
50
54
  self.is_cancel_job = Value('i', 0)
51
55
 
52
- if os.path.isdir(self.opt_input['dir']) == False:
53
- os.makedirs(self.opt_input['dir'])
56
+ if os.path.isdir(self.opt_input.dir) == False:
57
+ os.makedirs(self.opt_input.dir)
54
58
 
55
- if os.path.isdir(self.opt_output['dir']) == False:
56
- os.makedirs(self.opt_output['dir'])
59
+ if os.path.isdir(self.opt_output.dir) == False:
60
+ os.makedirs(self.opt_output.dir)
57
61
 
58
62
  def start(self) -> bool:
59
63
  self.cb_bar(set_progress_mode='indeterminate')
60
64
  self.cb_msg(cls=True)
61
65
 
62
66
  tasks = (
63
- self.sanitize,
64
67
  self.verify_input,
65
68
  self.cleanup,
66
69
  self.download,
@@ -77,33 +80,6 @@ class Flow:
77
80
  return 1
78
81
 
79
82
  return 0
80
-
81
- def sanitize(self) -> bool:
82
- def to_int(i):
83
- return i if i != None else None
84
-
85
- try:
86
- self.opt_comp['size_max']['img'] = to_int(self.opt_comp['size_max']['img'])
87
- self.opt_comp['size_max']['vid'] = to_int(self.opt_comp['size_max']['vid'])
88
- self.opt_comp['fps']['min'] = to_int(self.opt_comp['fps']['min'])
89
- self.opt_comp['fps']['max'] = to_int(self.opt_comp['fps']['max'])
90
- self.opt_comp['res']['w']['min'] = to_int(self.opt_comp['res']['w']['min'])
91
- self.opt_comp['res']['w']['max'] = to_int(self.opt_comp['res']['w']['max'])
92
- self.opt_comp['res']['h']['min'] = to_int(self.opt_comp['res']['h']['min'])
93
- self.opt_comp['res']['h']['max'] = to_int(self.opt_comp['res']['h']['max'])
94
- self.opt_comp['quality']['min'] = to_int(self.opt_comp['quality']['min'])
95
- self.opt_comp['quality']['max'] = to_int(self.opt_comp['quality']['max'])
96
- self.opt_comp['color']['min'] = to_int(self.opt_comp['color']['min'])
97
- self.opt_comp['color']['max'] = to_int(self.opt_comp['color']['max'])
98
- self.opt_comp['duration']['min'] = to_int(self.opt_comp['duration']['min'])
99
- self.opt_comp['duration']['max'] = to_int(self.opt_comp['duration']['max'])
100
- self.opt_comp['steps'] = to_int(self.opt_comp['steps'])
101
- self.opt_comp['processes'] = to_int(self.opt_comp['processes'])
102
- except ValueError:
103
- self.cb_msg('Non-numbers found in field(s). Check your input and try again.')
104
- return False
105
-
106
- return True
107
83
 
108
84
  def verify_input(self) -> bool:
109
85
  info_msg = ''
@@ -113,12 +89,12 @@ class Flow:
113
89
  save_to_local_tip += ' If you want to upload the results by yourself,\n'
114
90
  save_to_local_tip += ' select "Save to local directory only" for output\n'
115
91
 
116
- if self.opt_input['option'] == 'auto':
92
+ if self.opt_input.option == 'auto':
117
93
  error_msg += '\n'
118
94
  error_msg += '[X] Unrecognized URL input source\n'
119
95
 
120
- if (self.opt_input['option'] != 'local' and
121
- not self.opt_input.get('url')):
96
+ if (self.opt_input.option != 'local' and
97
+ not self.opt_input.url):
122
98
 
123
99
  error_msg += '\n'
124
100
  error_msg += '[X] URL address cannot be empty.\n'
@@ -127,37 +103,37 @@ class Flow:
127
103
  error_msg += ' in "Input source"\n'
128
104
 
129
105
 
130
- if ((self.opt_input.get('option') == 'telegram' or
131
- self.opt_output.get('option') == 'telegram') and
132
- not self.opt_cred.get('telegram', {}).get('token')):
106
+ if ((self.opt_input.option == 'telegram' or
107
+ self.opt_output.option == 'telegram') and
108
+ not self.opt_cred.telegram_token):
133
109
 
134
110
  error_msg += '[X] Downloading from and uploading to telegram requires bot token.\n'
135
111
  error_msg += save_to_local_tip
136
112
 
137
- if (self.opt_output.get('option') == 'telegram' and
138
- not self.opt_cred.get('telegram', {}).get('userid')):
113
+ if (self.opt_output.option == 'telegram' and
114
+ not self.opt_cred.telegram_userid):
139
115
 
140
116
  error_msg += '[X] Uploading to telegram requires user_id \n'
141
117
  error_msg += ' (From real account, not bot account).\n'
142
118
  error_msg += save_to_local_tip
143
119
 
144
120
 
145
- if (self.opt_output.get('option') == 'signal' and
146
- not (self.opt_cred.get('signal', {}).get('uuid') and self.opt_cred.get('signal', {}).get('password'))):
121
+ if (self.opt_output.option == 'signal' and
122
+ not (self.opt_cred.signal_uuid and self.opt_cred.signal_password)):
147
123
 
148
124
  error_msg += '[X] Uploading to signal requires uuid and password.\n'
149
125
  error_msg += save_to_local_tip
150
126
 
151
127
  output_presets = JsonManager.load_json('resources/output.json')
152
128
 
153
- input_option = self.opt_input.get('option')
154
- output_option = self.opt_output.get("option")
129
+ input_option = self.opt_input.option
130
+ output_option = self.opt_output.option
155
131
 
156
132
  for metadata in ('title', 'author'):
157
- if MetadataHandler.check_metadata_required(output_option, metadata) and not self.opt_output.get(metadata):
158
- if not MetadataHandler.check_metadata_provided(self.opt_input['dir'], input_option, metadata):
133
+ if MetadataHandler.check_metadata_required(output_option, metadata) and not getattr(self.opt_output, metadata):
134
+ if not MetadataHandler.check_metadata_provided(self.opt_input.dir, input_option, metadata):
159
135
  error_msg += f'[X] {output_presets[output_option]["full_name"]} requires {metadata}\n'
160
- if self.opt_input.get('option') == 'local':
136
+ if self.opt_input.option == 'local':
161
137
  error_msg += f' {metadata} was not supplied and {metadata}.txt is absent\n'
162
138
  else:
163
139
  error_msg += f' {metadata} was not supplied and input source will not provide {metadata}\n'
@@ -165,7 +141,7 @@ class Flow:
165
141
  error_msg += f' Create {metadata}.txt with the {metadata} name\n'
166
142
  else:
167
143
  info_msg += f'[!] {output_presets[output_option]["full_name"]} requires {metadata}\n'
168
- if self.opt_input.get('option') == 'local':
144
+ if self.opt_input.option == 'local':
169
145
  info_msg += f' {metadata} was not supplied but {metadata}.txt is present\n'
170
146
  info_msg += f' Using {metadata} name in {metadata}.txt\n'
171
147
  else:
@@ -183,10 +159,10 @@ class Flow:
183
159
  # Only warn if the compression option is available in export preset
184
160
  # Only warn if export option is not local or custom
185
161
  # Do not warn if no_compress is true
186
- if (not self.opt_comp['no_compress'] and
187
- self.opt_output['option'] != 'local' and
188
- self.opt_comp['preset'] != 'custom' and
189
- self.opt_output['option'] not in self.opt_comp['preset']):
162
+ if (not self.opt_comp.no_compress and
163
+ self.opt_output.option != 'local' and
164
+ self.opt_comp.preset != 'custom' and
165
+ self.opt_output.option not in self.opt_comp.preset):
190
166
 
191
167
  msg = 'Compression preset does not match export option\n'
192
168
  msg += 'You may continue, but the files will need to be compressed again before export\n'
@@ -198,9 +174,9 @@ class Flow:
198
174
  return False
199
175
 
200
176
  # Warn about unable to download animated Kakao stickers with such link
201
- if (self.opt_output.get('option') == 'kakao' and
202
- urlparse(self.opt_input.get('url')).netloc == 'e.kakao.com' and
203
- not self.opt_cred.get('kakao', {}).get('auth_token')):
177
+ if (self.opt_output.option == 'kakao' and
178
+ urlparse(self.opt_input.url).netloc == 'e.kakao.com' and
179
+ not self.opt_cred.kakao_auth_token):
204
180
 
205
181
  msg = 'To download ANIMATED stickers from e.kakao.com,\n'
206
182
  msg += 'you need to generate auth_token.\n'
@@ -229,32 +205,32 @@ class Flow:
229
205
  timestamp = datetime.now().strftime('%Y-%d-%m_%H-%M-%S')
230
206
  dir_name = 'archive_' + timestamp
231
207
 
232
- in_dir_files = [i for i in os.listdir(self.opt_input['dir']) if not i.startswith('archive_')]
233
- out_dir_files = [i for i in os.listdir(self.opt_output['dir']) if not i.startswith('archive_')]
208
+ in_dir_files = [i for i in os.listdir(self.opt_input.dir) if not i.startswith('archive_')]
209
+ out_dir_files = [i for i in os.listdir(self.opt_output.dir) if not i.startswith('archive_')]
234
210
 
235
- if self.opt_input['option'] == 'local':
211
+ if self.opt_input.option == 'local':
236
212
  self.cb_msg('Skip moving old files in input directory as input source is local')
237
213
  elif len(in_dir_files) == 0:
238
214
  self.cb_msg('Skip moving old files in input directory as input source is empty')
239
215
  else:
240
- archive_dir = os.path.join(self.opt_input['dir'], dir_name)
216
+ archive_dir = os.path.join(self.opt_input.dir, dir_name)
241
217
  self.cb_msg(f"Moving old files in input directory to {archive_dir} as input source is not local")
242
218
  os.makedirs(archive_dir)
243
219
  for i in in_dir_files:
244
- old_path = os.path.join(self.opt_input['dir'], i)
220
+ old_path = os.path.join(self.opt_input.dir, i)
245
221
  new_path = os.path.join(archive_dir, i)
246
222
  shutil.move(old_path, new_path)
247
223
 
248
- if self.opt_comp['no_compress']:
224
+ if self.opt_comp.no_compress:
249
225
  self.cb_msg('Skip moving old files in output directory as no_compress is True')
250
226
  elif len(out_dir_files) == 0:
251
227
  self.cb_msg('Skip moving old files in output directory as output source is empty')
252
228
  else:
253
- archive_dir = os.path.join(self.opt_output['dir'], dir_name)
229
+ archive_dir = os.path.join(self.opt_output.dir, dir_name)
254
230
  self.cb_msg(f"Moving old files in output directory to {archive_dir}")
255
231
  os.makedirs(archive_dir)
256
232
  for i in out_dir_files:
257
- old_path = os.path.join(self.opt_output['dir'], i)
233
+ old_path = os.path.join(self.opt_output.dir, i)
258
234
  new_path = os.path.join(archive_dir, i)
259
235
  shutil.move(old_path, new_path)
260
236
 
@@ -263,16 +239,16 @@ class Flow:
263
239
  def download(self) -> bool:
264
240
  downloaders = []
265
241
 
266
- if self.opt_input['option'] == 'signal':
242
+ if self.opt_input.option == 'signal':
267
243
  downloaders.append(DownloadSignal.start)
268
244
 
269
- if self.opt_input['option'] == 'line':
245
+ if self.opt_input.option == 'line':
270
246
  downloaders.append(DownloadLine.start)
271
247
 
272
- if self.opt_input['option'] == 'telegram':
248
+ if self.opt_input.option == 'telegram':
273
249
  downloaders.append(DownloadTelegram.start)
274
250
 
275
- if self.opt_input['option'] == 'kakao':
251
+ if self.opt_input.option == 'kakao':
276
252
  downloaders.append(DownloadKakao.start)
277
253
 
278
254
  if len(downloaders) > 0:
@@ -283,8 +259,8 @@ class Flow:
283
259
 
284
260
  for downloader in downloaders:
285
261
  success = downloader(
286
- url=self.opt_input['url'],
287
- out_dir=self.opt_input['dir'],
262
+ url=self.opt_input.url,
263
+ out_dir=self.opt_input.dir,
288
264
  opt_cred=self.opt_cred,
289
265
  cb_msg=self.cb_msg, cb_msg_block=self.cb_msg_block, cb_bar=self.cb_bar)
290
266
  self.cb_bar(set_progress_mode='indeterminate')
@@ -294,10 +270,10 @@ class Flow:
294
270
  return True
295
271
 
296
272
  def compress(self) -> bool:
297
- if self.opt_comp['no_compress'] == True:
273
+ if self.opt_comp.no_compress == True:
298
274
  self.cb_msg('no_compress is set to True, skip compression')
299
- in_dir_files = [i for i in sorted(os.listdir(self.opt_input['dir'])) if os.path.isfile(os.path.join(self.opt_input['dir'], i))]
300
- out_dir_files = [i for i in sorted(os.listdir(self.opt_output['dir'])) if os.path.isfile(os.path.join(self.opt_output['dir'], i))]
275
+ in_dir_files = [i for i in sorted(os.listdir(self.opt_input.dir)) if os.path.isfile(os.path.join(self.opt_input.dir, i))]
276
+ out_dir_files = [i for i in sorted(os.listdir(self.opt_output.dir)) if os.path.isfile(os.path.join(self.opt_output.dir, i))]
301
277
  if len(in_dir_files) == 0:
302
278
  self.cb_msg('Input directory is empty, nothing to copy to output directory')
303
279
  elif len(out_dir_files) != 0:
@@ -305,14 +281,14 @@ class Flow:
305
281
  else:
306
282
  self.cb_msg('Output directory is empty, copying files from input directory')
307
283
  for i in in_dir_files:
308
- src_f = os.path.join(self.opt_input['dir'], i)
309
- dst_f = os.path.join(self.opt_output['dir'], i)
284
+ src_f = os.path.join(self.opt_input.dir, i)
285
+ dst_f = os.path.join(self.opt_output.dir, i)
310
286
  shutil.copy(src_f, dst_f)
311
287
  return True
312
288
  msg = 'Compressing...'
313
289
 
314
- input_dir = self.opt_input['dir']
315
- output_dir = self.opt_output['dir']
290
+ input_dir = self.opt_input.dir
291
+ output_dir = self.opt_output.dir
316
292
 
317
293
  in_fs = []
318
294
 
@@ -338,9 +314,9 @@ class Flow:
338
314
  Thread(target=self.cb_msg_thread, args=(self.cb_msg_queue,)).start()
339
315
  Thread(target=self.processes_watcher_thread, args=(self.results_queue,)).start()
340
316
 
341
- for i in range(min(self.opt_comp['processes'], in_fs_count)):
317
+ for i in range(min(self.opt_comp.processes, in_fs_count)):
342
318
  process = Process(
343
- target=Flow.compress_worker,
319
+ target=Job.compress_worker,
344
320
  args=(self.jobs_queue, self.results_queue, self.cb_msg_queue),
345
321
  daemon=True
346
322
  )
@@ -351,10 +327,10 @@ class Flow:
351
327
  for i in in_fs:
352
328
  in_f = os.path.join(input_dir, i)
353
329
 
354
- if CodecInfo.is_anim(in_f) or self.opt_comp['fake_vid']:
355
- extension = self.opt_comp['format']['vid']
330
+ if CodecInfo.is_anim(in_f) or self.opt_comp.fake_vid:
331
+ extension = self.opt_comp.format_vid
356
332
  else:
357
- extension = self.opt_comp['format']['img']
333
+ extension = self.opt_comp.format_img
358
334
 
359
335
  out_f = os.path.join(output_dir, os.path.splitext(i)[0] + extension)
360
336
 
@@ -370,19 +346,32 @@ class Flow:
370
346
 
371
347
  return True
372
348
 
373
- def processes_watcher_thread(self, results_queue: Queue):
349
+ def processes_watcher_thread(
350
+ self,
351
+ results_queue: QueueType[Optional[tuple[bool, str, str, int]]]
352
+ ):
353
+
374
354
  for (success, in_f, out_f, size) in iter(results_queue.get, None): # type: ignore[misc]
375
355
  if success == False: # type: ignore
376
356
  self.compress_fails.append(in_f) # type: ignore[has-type]
377
357
 
378
358
  self.cb_bar(update_bar=True)
379
359
 
380
- def cb_msg_thread(self, cb_msg_queue: Queue):
360
+ def cb_msg_thread(
361
+ self,
362
+ cb_msg_queue: QueueType[Optional[str]]
363
+ ):
364
+
381
365
  for msg in iter(cb_msg_queue.get, None): # type: ignore
382
366
  self.cb_msg(msg)
383
367
 
384
368
  @staticmethod
385
- def compress_worker(jobs_queue: Queue, results_queue: Queue, cb_msg_queue: Queue):
369
+ def compress_worker(
370
+ jobs_queue: QueueType[Optional[tuple[str, str, CompOption]]],
371
+ results_queue: QueueType[Optional[tuple[bool, str, str, int]]],
372
+ cb_msg_queue: QueueType[Optional[str]]
373
+ ):
374
+
386
375
  for (in_f, out_f, opt_comp) in iter(jobs_queue.get, None): # type: ignore[misc]
387
376
  sticker = StickerConvert(in_f, out_f, opt_comp, cb_msg_queue) # type: ignore
388
377
  success, in_f, out_f, size = sticker.convert()
@@ -394,7 +383,7 @@ class Flow:
394
383
  def export(self) -> bool:
395
384
  self.cb_bar(set_progress_mode='indeterminate')
396
385
 
397
- if self.opt_output['option'] == 'local':
386
+ if self.opt_output.option == 'local':
398
387
  self.cb_msg('Saving to local directory only, nothing to export')
399
388
  return True
400
389
 
@@ -402,16 +391,16 @@ class Flow:
402
391
 
403
392
  exporters: list[UploadBase] = []
404
393
 
405
- if self.opt_output['option'] == 'whatsapp':
394
+ if self.opt_output.option == 'whatsapp':
406
395
  exporters.append(CompressWastickers.start)
407
396
 
408
- if self.opt_output['option'] == 'signal':
397
+ if self.opt_output.option == 'signal':
409
398
  exporters.append(UploadSignal.start)
410
399
 
411
- if self.opt_output['option'] == 'telegram':
400
+ if self.opt_output.option == 'telegram':
412
401
  exporters.append(UploadTelegram.start)
413
402
 
414
- if self.opt_output['option'] == 'imessage':
403
+ if self.opt_output.option == 'imessage':
415
404
  exporters.append(XcodeImessage.start)
416
405
 
417
406
  for exporter in exporters:
@@ -420,7 +409,7 @@ class Flow:
420
409
  cb_msg=self.cb_msg, cb_msg_block=self.cb_msg_block, cb_ask_bool=self.cb_ask_bool, cb_bar=self.cb_bar)
421
410
 
422
411
  if self.out_urls:
423
- with open(os.path.join(self.opt_output['dir'], 'export-result.txt'), 'w+') as f:
412
+ with open(os.path.join(self.opt_output.dir, 'export-result.txt'), 'w+') as f:
424
413
  f.writelines(self.out_urls)
425
414
  else:
426
415
  self.cb_msg('An error occured while exporting stickers')