sticker-convert 2.12.3__py3-none-any.whl → 2.13.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 (32) hide show
  1. sticker_convert/cli.py +3 -0
  2. sticker_convert/converter.py +61 -54
  3. sticker_convert/downloaders/download_band.py +110 -0
  4. sticker_convert/downloaders/download_kakao.py +84 -22
  5. sticker_convert/gui.py +14 -3
  6. sticker_convert/gui_components/windows/advanced_compression_window.py +17 -0
  7. sticker_convert/gui_components/windows/discord_get_auth_window.py +3 -3
  8. sticker_convert/gui_components/windows/kakao_get_auth_window.py +2 -6
  9. sticker_convert/gui_components/windows/signal_get_auth_window.py +3 -3
  10. sticker_convert/gui_components/windows/viber_get_auth_window.py +1 -1
  11. sticker_convert/job.py +6 -0
  12. sticker_convert/job_option.py +2 -0
  13. sticker_convert/resources/compression.json +47 -0
  14. sticker_convert/resources/help.json +1 -0
  15. sticker_convert/resources/input.json +10 -0
  16. sticker_convert/resources/memdump_linux.sh +0 -1
  17. sticker_convert/utils/auth/get_discord_auth.py +1 -1
  18. sticker_convert/utils/auth/get_kakao_desktop_auth.py +119 -35
  19. sticker_convert/utils/auth/get_signal_auth.py +2 -2
  20. sticker_convert/utils/auth/get_viber_auth.py +1 -1
  21. sticker_convert/utils/auth/telethon_setup.py +3 -1
  22. sticker_convert/utils/chrome_remotedebug.py +25 -13
  23. sticker_convert/utils/media/codec_info.py +1 -1
  24. sticker_convert/utils/singletons.py +18 -0
  25. sticker_convert/utils/url_detect.py +3 -0
  26. sticker_convert/version.py +1 -1
  27. {sticker_convert-2.12.3.dist-info → sticker_convert-2.13.0.dist-info}/METADATA +35 -27
  28. {sticker_convert-2.12.3.dist-info → sticker_convert-2.13.0.dist-info}/RECORD +32 -30
  29. {sticker_convert-2.12.3.dist-info → sticker_convert-2.13.0.dist-info}/WHEEL +1 -1
  30. {sticker_convert-2.12.3.dist-info → sticker_convert-2.13.0.dist-info}/entry_points.txt +0 -0
  31. {sticker_convert-2.12.3.dist-info → sticker_convert-2.13.0.dist-info}/licenses/LICENSE +0 -0
  32. {sticker_convert-2.12.3.dist-info → sticker_convert-2.13.0.dist-info}/top_level.txt +0 -0
sticker_convert/job.py CHANGED
@@ -12,6 +12,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
12
12
  from urllib.parse import urlparse
13
13
 
14
14
  from sticker_convert.converter import StickerConvert
15
+ from sticker_convert.downloaders.download_band import DownloadBand
15
16
  from sticker_convert.downloaders.download_discord import DownloadDiscord
16
17
  from sticker_convert.downloaders.download_kakao import DownloadKakao
17
18
  from sticker_convert.downloaders.download_line import DownloadLine
@@ -28,6 +29,7 @@ from sticker_convert.utils.callback import CallbackReturn, CbQueueType, ResultsL
28
29
  from sticker_convert.utils.files.json_resources_loader import OUTPUT_JSON
29
30
  from sticker_convert.utils.files.metadata_handler import MetadataHandler
30
31
  from sticker_convert.utils.media.codec_info import CodecInfo
32
+ from sticker_convert.utils.singletons import singletons
31
33
 
32
34
 
33
35
  class Executor:
@@ -136,6 +138,7 @@ class Executor:
136
138
 
137
139
  work_queue.put(None)
138
140
  cb_queue.put("__PROCESS_DONE__")
141
+ singletons.close()
139
142
 
140
143
  def start_workers(self, processes: int = 1) -> None:
141
144
  self.cb_thread_instance = Thread(
@@ -564,6 +567,9 @@ class Job:
564
567
  if self.opt_input.option == "kakao":
565
568
  downloaders.append(DownloadKakao.start)
566
569
 
570
+ if self.opt_input.option == "band":
571
+ downloaders.append(DownloadBand.start)
572
+
567
573
  if self.opt_input.option == "viber":
568
574
  downloaders.append(DownloadViber.start)
569
575
 
@@ -75,6 +75,7 @@ class CompOption(BaseOption):
75
75
  fake_vid: Optional[bool] = None
76
76
  quantize_method: Optional[str] = None
77
77
  scale_filter: Optional[str] = None
78
+ chromium_path: Optional[str] = None
78
79
  cache_dir: Optional[str] = None
79
80
  default_emoji: str = "😀"
80
81
  no_compress: Optional[bool] = None
@@ -109,6 +110,7 @@ class CompOption(BaseOption):
109
110
  "fake_vid": self.fake_vid,
110
111
  "quantize_method": self.quantize_method,
111
112
  "scale_filter": self.scale_filter,
113
+ "chromium_path": self.chromium_path,
112
114
  "cache_dir": self.cache_dir,
113
115
  "default_emoji": self.default_emoji,
114
116
  "no_compress": self.no_compress,
@@ -328,6 +328,53 @@
328
328
  "quantize_method": "imagequant",
329
329
  "default_emoji": "😀"
330
330
  },
331
+ "band": {
332
+ "size_max": {
333
+ "img": 0,
334
+ "vid": 0
335
+ },
336
+ "format": {
337
+ "img": ".png",
338
+ "vid": ".png"
339
+ },
340
+ "fps": {
341
+ "min": 1,
342
+ "max": 30,
343
+ "power": -0.5
344
+ },
345
+ "res": {
346
+ "w": {
347
+ "min": 100,
348
+ "max": 370
349
+ },
350
+ "h": {
351
+ "min": 100,
352
+ "max": 320
353
+ },
354
+ "power": 3
355
+ },
356
+ "quality": {
357
+ "min": 10,
358
+ "max": 95,
359
+ "power": 5
360
+ },
361
+ "color": {
362
+ "min": 32,
363
+ "max": 257,
364
+ "power": 3
365
+ },
366
+ "duration": {
367
+ "min": 83,
368
+ "max": 4000
369
+ },
370
+ "padding_percent": 0,
371
+ "bg_color": "",
372
+ "steps": 16,
373
+ "fake_vid": false,
374
+ "scale_filter": "bicubic",
375
+ "quantize_method": "imagequant",
376
+ "default_emoji": "😀"
377
+ },
331
378
  "viber": {
332
379
  "size_max": {
333
380
  "img": 0,
@@ -52,6 +52,7 @@
52
52
  "scale_filter": "Set scale filter. Default as bicubic. Valid options are:\n- nearest = Use nearest neighbour (Suitable for pixel art)\n- box = Similar to nearest, but better downscaling\n- bilinear = Linear interpolation\n- hamming = Similar to bilinear, but better downscaling\n- bicubic = Cubic spline interpolation\n- lanczos = A high-quality downsampling filter",
53
53
  "quantize_method": "Set method for quantizing image. Default as imagequant. Valid options are:\n- imagequant = Best quality but slow\n- fastoctree = Fast but image looks chunky\n- none = No image quantizing, large image size as result",
54
54
  "cache_dir": "Set custom cache directory.\nUseful for debugging, or speed up conversion if cache_dir is on RAM disk.",
55
+ "chromium_path": "Set Chromium(-based)/Chrome browser path.\nRequired for converting from SVG files.\nLeave blank to auto detect",
55
56
  "default_emoji": "Set the default emoji for uploading Signal and Telegram sticker packs."
56
57
  },
57
58
  "cred": {
@@ -59,6 +59,16 @@
59
59
  "author": true
60
60
  }
61
61
  },
62
+ "band": {
63
+ "full_name": "Download from Naver Band",
64
+ "help": "Download Naver Band stickers from a URL / ID as input",
65
+ "example": "Example: https://www.band.us/sticker/xxxx OR 2535",
66
+ "address_lbls": "URL address / ID",
67
+ "metadata_provides": {
68
+ "title": true,
69
+ "author": false
70
+ }
71
+ },
62
72
  "viber": {
63
73
  "full_name": "Download from Viber",
64
74
  "help": "Download viber stickers from a URL as input",
@@ -7,7 +7,6 @@ PID=$1
7
7
  PID_MAPS=/proc/$PID/maps
8
8
  PID_MEM=/proc/$PID/mem
9
9
 
10
- rm -f /tmp/viber.dmp.$PID
11
10
  grep rw-p $PID_MAPS |
12
11
  while IFS='' read -r line || [[ -n "$line" ]]; do
13
12
  range=`echo $line | awk '{print $1;}'`
@@ -13,7 +13,7 @@ from sticker_convert.utils.process import killall
13
13
 
14
14
 
15
15
  class GetDiscordAuth:
16
- def __init__(self, cb_msg: Callable[..., None] = print):
16
+ def __init__(self, cb_msg: Callable[..., None] = print) -> None:
17
17
  chromedriver_download_dir = CONFIG_DIR / "bin"
18
18
  os.makedirs(chromedriver_download_dir, exist_ok=True)
19
19
 
@@ -18,21 +18,33 @@ then login to Kakao Desktop and try again."""
18
18
  MSG_NO_AUTH = """Kakao Desktop installed,
19
19
  but kakao_auth not found.
20
20
  Please login to Kakao Desktop and try again."""
21
+ MSG_SIP_ENABLED = """You need to disable SIP:
22
+ 1. Restart computer in Recovery mode
23
+ 2. Launch Terminal from the Utilities menu
24
+ 3. Run the command `csrutil disable`
25
+ 4. Restart your computer"""
21
26
  MSG_LAUNCH_FAIL = "Failed to launch Kakao"
22
27
  MSG_PERMISSION_ERROR = "Failed to read Kakao process memory"
23
- MSG_UNSUPPORTED = "Only Windows is supported for this method"
24
28
 
25
29
 
26
30
  class GetKakaoDesktopAuth:
27
- def __init__(self, cb_ask_str: Callable[..., str] = input):
31
+ def __init__(self, cb_ask_str: Callable[..., str] = input) -> None:
28
32
  self.cb_ask_str = cb_ask_str
29
33
 
34
+ def launch_kakao(self, kakao_bin_path: str) -> None:
35
+ if platform.system() == "Windows":
36
+ subprocess.Popen([kakao_bin_path])
37
+ elif platform.system() == "Darwin":
38
+ subprocess.Popen(["open", kakao_bin_path])
39
+ else:
40
+ subprocess.Popen(["wine", kakao_bin_path])
41
+
30
42
  def relaunch_kakao(self, kakao_bin_path: str) -> Optional[int]:
31
43
  killed = killall("kakaotalk")
32
44
  if killed:
33
45
  time.sleep(5)
34
46
 
35
- subprocess.Popen([kakao_bin_path])
47
+ self.launch_kakao(kakao_bin_path)
36
48
  time.sleep(20)
37
49
 
38
50
  return find_pid_by_name("kakaotalk")
@@ -54,7 +66,7 @@ class GetKakaoDesktopAuth:
54
66
  try:
55
67
  with OpenProcess(pid=int(kakao_pid)) as process:
56
68
  for address in process.search_by_value( # type: ignore
57
- str, 15, "\x00authorization\x00"
69
+ str, 15, "authorization: "
58
70
  ):
59
71
  auth_token_addr = cast(int, address) + 15
60
72
  auth_token_bytes = process.read_process_memory(
@@ -127,11 +139,77 @@ class GetKakaoDesktopAuth:
127
139
 
128
140
  return auth_token, msg
129
141
 
130
- def get_kakao_desktop(self) -> Optional[str]:
131
- kakao_bin_path = os.path.expandvars(
132
- "%programfiles(x86)%\\Kakao\\KakaoTalk\\KakaoTalk.exe"
142
+ def get_auth_darwin(self, kakao_bin_path: str) -> Tuple[Optional[str], str]:
143
+ killall("kakaotalk")
144
+
145
+ subprocess.run(
146
+ [
147
+ "lldb",
148
+ kakao_bin_path,
149
+ "-o",
150
+ "b ptrace",
151
+ "-o",
152
+ "r",
153
+ "-o",
154
+ "thread return",
155
+ "-o",
156
+ "con",
157
+ "-o",
158
+ "process save-core /tmp/memdump.kakaotalk.dmp",
159
+ "-o",
160
+ "con",
161
+ "-o",
162
+ "quit",
163
+ ],
164
+ stdout=subprocess.DEVNULL,
165
+ stderr=subprocess.DEVNULL,
133
166
  )
134
- if Path(kakao_bin_path).is_file():
167
+
168
+ with open("/tmp/memdump.kakaotalk.dmp", "rb") as f:
169
+ mem = f.read()
170
+
171
+ os.remove("/tmp/memdump.kakaotalk.dmp")
172
+
173
+ auth_token = None
174
+ for i in re.finditer(b"]mac/", mem):
175
+ auth_token_term = i.start()
176
+
177
+ auth_token_bytes = mem[auth_token_term - 200 : auth_token_term]
178
+ auth_token_start = auth_token_bytes.find(b"application/json_\x10\x8a") + 19
179
+ if auth_token_start == -1:
180
+ continue
181
+ try:
182
+ auth_token_candidate = auth_token_bytes[auth_token_start:].decode(
183
+ encoding="ascii"
184
+ )
185
+ except UnicodeDecodeError:
186
+ continue
187
+
188
+ if 150 > len(auth_token_candidate) > 100:
189
+ auth_token = auth_token_candidate
190
+ break
191
+
192
+ if auth_token is None:
193
+ return None, MSG_NO_AUTH
194
+ else:
195
+ msg = "Got auth_token successfully:\n"
196
+ msg += f"{auth_token=}\n"
197
+
198
+ return auth_token, msg
199
+
200
+ def get_kakao_desktop(self) -> Optional[str]:
201
+ if platform.system() == "Windows":
202
+ kakao_bin_path = os.path.expandvars(
203
+ "%programfiles(x86)%\\Kakao\\KakaoTalk\\KakaoTalk.exe"
204
+ )
205
+ elif platform.system() == "Darwin":
206
+ kakao_bin_path = "/Applications/KakaoTalk.app"
207
+ else:
208
+ kakao_bin_path = os.path.expanduser(
209
+ "~/.wine/drive_c/Program Files (x86)/Kakao/KakaoTalk/KakaoTalk.exe"
210
+ )
211
+
212
+ if Path(kakao_bin_path).exists():
135
213
  return kakao_bin_path
136
214
 
137
215
  return None
@@ -140,15 +218,6 @@ class GetKakaoDesktopAuth:
140
218
  self,
141
219
  kakao_bin_path: Optional[str] = None,
142
220
  ) -> Tuple[Optional[str], str]:
143
- if platform.system() != "Windows":
144
- return None, MSG_UNSUPPORTED
145
-
146
- if not kakao_bin_path:
147
- kakao_bin_path = self.get_kakao_desktop()
148
-
149
- if not kakao_bin_path:
150
- return None, MSG_NO_BIN
151
-
152
221
  # get_auth_by_dump()
153
222
  # + Fast
154
223
  # - Requires admin
@@ -158,23 +227,38 @@ class GetKakaoDesktopAuth:
158
227
  # - Slow
159
228
  # - Cannot run on macOS
160
229
 
161
- # If admin, prefer get_auth_by_dump() over get_auth_by_pme(), vice versa
162
- methods: List[Callable[[str, bool], Tuple[Optional[str], str]]] = []
163
- relaunch = True
164
- kakao_auth = None
165
- msg = ""
166
-
167
- pme_present = importlib.util.find_spec("PyMemoryEditor") is not None
168
- methods.append(self.get_auth_by_dump)
169
- if pme_present:
170
- methods.append(self.get_auth_by_pme)
171
- if check_admin() is False:
172
- methods.reverse()
173
-
174
- for method in methods:
175
- kakao_auth, msg = method(kakao_bin_path, relaunch)
176
- relaunch = False
177
- if kakao_auth is not None:
178
- break
230
+ if not kakao_bin_path:
231
+ kakao_bin_path = self.get_kakao_desktop()
232
+
233
+ if not kakao_bin_path:
234
+ return None, MSG_NO_BIN
235
+
236
+ if platform.system() != "Darwin":
237
+ # If admin, prefer get_auth_by_dump() over get_auth_by_pme(), vice versa
238
+ methods: List[Callable[[str, bool], Tuple[Optional[str], str]]] = []
239
+ relaunch = True
240
+ kakao_auth = None
241
+ msg = ""
242
+
243
+ pme_present = importlib.util.find_spec("PyMemoryEditor") is not None
244
+ methods.append(self.get_auth_by_dump)
245
+ if pme_present:
246
+ methods.append(self.get_auth_by_pme)
247
+ if check_admin() is False:
248
+ methods.reverse()
249
+
250
+ for method in methods:
251
+ kakao_auth, msg = method(kakao_bin_path, relaunch)
252
+ relaunch = False
253
+ if kakao_auth is not None:
254
+ break
255
+ else:
256
+ csrutil_status = subprocess.run(
257
+ ["csrutil", "status"], capture_output=True, text=True
258
+ ).stdout
259
+
260
+ if "enabled" in csrutil_status:
261
+ return None, MSG_SIP_ENABLED
262
+ kakao_auth, msg = self.get_auth_darwin(kakao_bin_path)
179
263
 
180
264
  return kakao_auth, msg
@@ -17,7 +17,7 @@ class GetSignalAuth:
17
17
  self,
18
18
  cb_msg: Callable[..., None] = print,
19
19
  cb_ask_str: Callable[..., str] = input,
20
- ):
20
+ ) -> None:
21
21
  chromedriver_download_dir = CONFIG_DIR / "bin"
22
22
  os.makedirs(chromedriver_download_dir, exist_ok=True)
23
23
 
@@ -26,7 +26,7 @@ class GetSignalAuth:
26
26
  self.cb_ask_str = cb_ask_str
27
27
  self.cb_msg = cb_msg
28
28
 
29
- def download_signal_desktop(self):
29
+ def download_signal_desktop(self) -> None:
30
30
  download_url = "https://signal.org/en/download/"
31
31
 
32
32
  webbrowser.open(download_url)
@@ -28,7 +28,7 @@ MSG_PERMISSION_ERROR = "Failed to read Viber process memory"
28
28
 
29
29
 
30
30
  class GetViberAuth:
31
- def __init__(self, cb_ask_str: Callable[..., str] = input):
31
+ def __init__(self, cb_ask_str: Callable[..., str] = input) -> None:
32
32
  self.cb_ask_str = cb_ask_str
33
33
 
34
34
  def relaunch_viber(self, viber_bin_path: str) -> Optional[int]:
@@ -22,7 +22,9 @@ Continue when done"""
22
22
 
23
23
 
24
24
  class TelethonSetup:
25
- def __init__(self, opt_cred: CredOption, cb_ask_str: Callable[..., str] = input):
25
+ def __init__(
26
+ self, opt_cred: CredOption, cb_ask_str: Callable[..., str] = input
27
+ ) -> None:
26
28
  self.cb_ask_str = cb_ask_str
27
29
  self.opt_cred = opt_cred
28
30
 
@@ -5,12 +5,12 @@ import json
5
5
  import os
6
6
  import platform
7
7
  import shutil
8
+ import signal
8
9
  import socket
9
10
  import subprocess
10
11
  import time
11
12
  from typing import Any, Dict, List, Optional, Tuple, Union, cast
12
13
 
13
- import browsers # type: ignore
14
14
  import requests
15
15
  import websocket
16
16
  from PIL import Image
@@ -50,7 +50,7 @@ class CRD:
50
50
  chrome_bin: str,
51
51
  port: Optional[int] = None,
52
52
  args: Optional[List[str]] = None,
53
- ):
53
+ ) -> None:
54
54
  if port is None:
55
55
  port = get_free_port()
56
56
  self.port = port
@@ -84,6 +84,14 @@ class CRD:
84
84
 
85
85
  @staticmethod
86
86
  def get_chrome_path() -> Optional[str]:
87
+ import logging
88
+
89
+ import browsers # type: ignore
90
+
91
+ # browsers module would turn on info logging
92
+ logger = logging.getLogger()
93
+ logger.setLevel(logging.CRITICAL)
94
+
87
95
  bs: List[Tuple[int, str]] = []
88
96
  for b in browsers.browsers():
89
97
  browser_type = b["browser_type"]
@@ -98,7 +106,7 @@ class CRD:
98
106
  bs = sorted(bs, key=lambda x: x[0])
99
107
  return bs[0][1]
100
108
 
101
- def connect(self, target_id: int = 0):
109
+ def connect(self, target_id: int = 0) -> None:
102
110
  self.cmd_id = 1
103
111
  r = None
104
112
  targets: List[Any] = []
@@ -137,7 +145,7 @@ class CRD:
137
145
 
138
146
  raise RuntimeError("Websocket keep disconnecting")
139
147
 
140
- def exec_js(self, js: str, context_id: Optional[int] = None):
148
+ def exec_js(self, js: str, context_id: Optional[int] = None) -> Union[str, bytes]:
141
149
  command: Dict[str, Any] = {
142
150
  "id": self.cmd_id,
143
151
  "method": "Runtime.evaluate",
@@ -155,11 +163,11 @@ class CRD:
155
163
  }
156
164
  return self.send_cmd(command)
157
165
 
158
- def screenshot(self, clip: Optional[Dict[str, int]] = None):
166
+ def screenshot(self, clip: Optional[Dict[str, int]] = None) -> Image.Image:
159
167
  command: Dict[str, Any] = {
160
168
  "id": self.cmd_id,
161
169
  "method": "Page.captureScreenshot",
162
- "params": {},
170
+ "params": {"captureBeyondViewport": True, "optimizeForSpeed": True},
163
171
  }
164
172
  if clip:
165
173
  command["params"]["clip"] = clip
@@ -174,11 +182,11 @@ class CRD:
174
182
  str, json.loads(r).get("result", {}).get("result", {}).get("value", "")
175
183
  )
176
184
 
177
- def navigate(self, url: str):
185
+ def navigate(self, url: str) -> None:
178
186
  command = {"id": self.cmd_id, "method": "Page.navigate", "params": {"url": url}}
179
187
  self.send_cmd(command)
180
188
 
181
- def open_html_str(self, html: str):
189
+ def open_html_str(self, html: str) -> None:
182
190
  command: Dict[str, Any] = {
183
191
  "id": self.cmd_id,
184
192
  "method": "Page.navigate",
@@ -198,24 +206,28 @@ class CRD:
198
206
  }
199
207
  self.send_cmd(command)
200
208
 
201
- def runtime_enable(self):
209
+ def runtime_enable(self) -> None:
202
210
  command = {
203
211
  "method": "Runtime.enable",
204
212
  }
205
213
  self.send_cmd(command)
206
214
 
207
- def runtime_disable(self):
215
+ def runtime_disable(self) -> None:
208
216
  command = {
209
217
  "method": "Runtime.disable",
210
218
  }
211
219
  self.send_cmd(command)
212
220
 
213
- def reload(self):
221
+ def reload(self) -> None:
214
222
  command = {
215
223
  "method": "Page.reload",
216
224
  }
217
225
  self.send_cmd(command)
218
226
 
219
- def close(self):
227
+ def close(self) -> None:
228
+ command = {
229
+ "method": "Browser.close",
230
+ }
231
+ self.send_cmd(command)
220
232
  self.ws.close()
221
- self.chrome_proc.kill()
233
+ os.kill(self.chrome_proc.pid, signal.SIGTERM)
@@ -20,7 +20,7 @@ from sticker_convert.definitions import SVG_DEFAULT_HEIGHT, SVG_DEFAULT_WIDTH, S
20
20
  warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
21
21
 
22
22
 
23
- def lcm(a: int, b: int):
23
+ def lcm(a: int, b: int) -> int:
24
24
  return abs(a * b) // gcd(a, b)
25
25
 
26
26
 
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ from typing import Any, Dict, Protocol
3
+
4
+
5
+ class SingletonProtocol(Protocol):
6
+ def close(self) -> Any: ...
7
+
8
+
9
+ class Singletons:
10
+ def __init__(self) -> None:
11
+ self.objs: Dict[str, SingletonProtocol] = {}
12
+
13
+ def close(self) -> None:
14
+ for obj in self.objs.values():
15
+ obj.close()
16
+
17
+
18
+ singletons = Singletons()
@@ -25,6 +25,9 @@ class UrlDetect:
25
25
  if domain in ("e.kakao.com", "emoticon.kakao.com"):
26
26
  return "kakao"
27
27
 
28
+ if domain == "www.band.us":
29
+ return "band"
30
+
28
31
  if domain == "stickers.viber.com":
29
32
  return "viber"
30
33
 
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- __version__ = "2.12.3"
3
+ __version__ = "2.13.0"