sticker-convert 2.8.12__py3-none-any.whl → 2.8.14__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.
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import annotations
3
3
 
4
+ import itertools
4
5
  import json
6
+ import re
5
7
  import zipfile
6
8
  from io import BytesIO
7
9
  from pathlib import Path
8
- from typing import Any, List, Optional, Tuple
10
+ from typing import Any, List, Optional, Tuple, cast
9
11
  from urllib.parse import urlparse
10
12
 
11
13
  import requests
@@ -19,6 +21,26 @@ from sticker_convert.utils.files.metadata_handler import MetadataHandler
19
21
  from sticker_convert.utils.media.decrypt_kakao import DecryptKakao
20
22
 
21
23
 
24
+ def search_bracket(text: str, open_bracket: str = "{", close_bracket: str = "}") -> int:
25
+ depth = 0
26
+ is_str = False
27
+
28
+ for count, char in enumerate(text):
29
+ if char == '"':
30
+ is_str = not is_str
31
+
32
+ if is_str is False:
33
+ if char == open_bracket:
34
+ depth += 1
35
+ elif char == close_bracket:
36
+ depth -= 1
37
+
38
+ if depth == 0:
39
+ return count
40
+
41
+ return -1
42
+
43
+
22
44
  class MetadataKakao:
23
45
  @staticmethod
24
46
  def get_info_from_share_link(url: str) -> Tuple[Optional[str], Optional[str]]:
@@ -36,34 +58,55 @@ class MetadataKakao:
36
58
  app_scheme_link_tag = soup.find("a", id="app_scheme_link") # type: ignore
37
59
  assert isinstance(app_scheme_link_tag, Tag)
38
60
 
39
- data_urls = app_scheme_link_tag.get("data-url")
40
- if not data_urls:
41
- return None, None
42
- if isinstance(data_urls, list):
43
- data_url = data_urls[0]
44
- else:
45
- data_url = data_urls
46
-
47
- item_code = data_url.replace("kakaotalk://store/emoticon/", "").split("?")[0]
61
+ item_code_fake = cast(str, app_scheme_link_tag["data-i"])
48
62
 
49
- return pack_title, item_code
63
+ js = ""
64
+ for script_tag in soup.find_all("script"):
65
+ js = script_tag.string
66
+ if js and "emoticonDeepLink" in js:
67
+ break
68
+ if "emoticonDeepLink" not in js:
69
+ return None, None
50
70
 
51
- @staticmethod
52
- def get_info_from_pack_title(
53
- pack_title: str,
54
- ) -> Tuple[Optional[str], Optional[str], Optional[str]]:
55
- pack_meta_r = requests.get(f"https://e.kakao.com/api/v1/items/t/{pack_title}")
71
+ func_start_pos = js.find("function emoticonDeepLink(")
72
+ js = js[func_start_pos:]
73
+ bracket_start_pos = js.find("{")
74
+ func_end_pos = search_bracket(js[bracket_start_pos:]) + bracket_start_pos
75
+ js = js[bracket_start_pos + 1 : func_end_pos]
76
+ js = js.split(";")[0]
56
77
 
57
- if pack_meta_r.status_code == 200:
58
- pack_meta = json.loads(pack_meta_r.text)
59
- else:
60
- return None, None, None
78
+ minus_num_regex = re.search(r"\-(.*?)\^", js)
79
+ if not minus_num_regex:
80
+ return None, None
81
+ minus_num_str = minus_num_regex.group(1)
82
+ if not minus_num_str.isnumeric():
83
+ return None, None
84
+ minus_num = int(minus_num_str)
61
85
 
62
- author = pack_meta["result"]["artist"]
63
- title_ko = pack_meta["result"]["title"]
64
- thumbnail_urls = pack_meta["result"]["thumbnailUrls"]
86
+ xor_num_regex = re.search(r"\^(.*?)\)", js)
87
+ if not xor_num_regex:
88
+ return None, None
89
+ xor_num_str = xor_num_regex.group(1)
90
+ if not xor_num_str.isnumeric():
91
+ return None, None
92
+ xor_num = int(xor_num_str)
93
+
94
+ item_code = str(int(item_code_fake) - minus_num ^ xor_num)
95
+
96
+ # https://github.com/Nuitka/Nuitka/issues/385
97
+ # js2py not working if compiled by nuitka
98
+ # web2app_start_pos = js.find("daumtools.web2app(")
99
+ # js = js[:web2app_start_pos] + "return a;}"
100
+ # get_item_code = js2py.eval_js(js) # type: ignore
101
+ # kakao_scheme_link = cast(
102
+ # str,
103
+ # get_item_code(
104
+ # "kakaotalk://store/emoticon/${i}?referer=share_link", item_code_fake
105
+ # ),
106
+ # )
107
+ # item_code = urlparse(kakao_scheme_link).path.split("/")[-1]
65
108
 
66
- return author, title_ko, thumbnail_urls
109
+ return pack_title, item_code
67
110
 
68
111
  @staticmethod
69
112
  def get_item_code(title_ko: str, auth_token: str) -> Optional[str]:
@@ -88,9 +131,27 @@ class MetadataKakao:
88
131
  return item_code
89
132
 
90
133
  @staticmethod
91
- def get_title_from_id(item_code: str, auth_token: str) -> Optional[str]:
134
+ def get_pack_info_unauthed(
135
+ pack_title: str,
136
+ ) -> Optional[dict[str, Any]]:
137
+ pack_meta_r = requests.get(f"https://e.kakao.com/api/v1/items/t/{pack_title}")
138
+
139
+ if pack_meta_r.status_code == 200:
140
+ pack_meta = json.loads(pack_meta_r.text)
141
+ else:
142
+ return None
143
+
144
+ return pack_meta
145
+
146
+ @staticmethod
147
+ def get_pack_info_authed(
148
+ item_code: str, auth_token: str
149
+ ) -> Optional[dict[str, Any]]:
92
150
  headers = {
93
151
  "Authorization": auth_token,
152
+ "Talk-Agent": "android/10.8.1",
153
+ "Talk-Language": "en",
154
+ "User-Agent": "okhttp/4.10.0",
94
155
  }
95
156
 
96
157
  response = requests.post(
@@ -102,11 +163,8 @@ class MetadataKakao:
102
163
  return None
103
164
 
104
165
  response_json = json.loads(response.text)
105
- title = response_json["itemUnitInfo"][0]["title"]
106
- # play_path_format = response_json['itemUnitInfo'][0]['playPathFormat']
107
- # stickers_count = len(response_json['itemUnitInfo'][0]['sizes'])
108
166
 
109
- return title
167
+ return response_json
110
168
 
111
169
 
112
170
  class DownloadKakao(DownloadBase):
@@ -114,11 +172,15 @@ class DownloadKakao(DownloadBase):
114
172
  super().__init__(*args, **kwargs)
115
173
  self.pack_title: Optional[str] = None
116
174
  self.author: Optional[str] = None
175
+ self.auth_token: Optional[str] = None
176
+
177
+ self.pack_info_unauthed: Optional[dict[str, Any]] = None
178
+ self.pack_info_authed: Optional[dict[str, Any]] = None
117
179
 
118
180
  def download_stickers_kakao(self) -> bool:
119
- auth_token = None
181
+ self.auth_token = None
120
182
  if self.opt_cred:
121
- auth_token = self.opt_cred.kakao_auth_token
183
+ self.auth_token = self.opt_cred.kakao_auth_token
122
184
 
123
185
  if urlparse(self.url).netloc == "emoticon.kakao.com":
124
186
  self.pack_title, item_code = MetadataKakao.get_info_from_share_link(
@@ -134,9 +196,13 @@ class DownloadKakao(DownloadBase):
134
196
  item_code = self.url.replace("kakaotalk://store/emoticon/", "")
135
197
 
136
198
  self.pack_title = None
137
- if auth_token:
138
- self.pack_title = MetadataKakao.get_title_from_id(item_code, auth_token)
139
- if not self.pack_title:
199
+ if self.auth_token:
200
+ self.pack_info_authed = MetadataKakao.get_pack_info_authed(
201
+ item_code, self.auth_token
202
+ )
203
+ if self.pack_info_authed:
204
+ self.pack_title = self.pack_info_authed["itemUnitInfo"][0]["title"]
205
+ else:
140
206
  self.cb.put("Warning: Cannot get pack_title with auth_token.")
141
207
  self.cb.put(
142
208
  "Is auth_token invalid / expired? Try to regenerate it."
@@ -146,25 +212,23 @@ class DownloadKakao(DownloadBase):
146
212
  return self.download_animated(item_code)
147
213
 
148
214
  if urlparse(self.url).netloc == "e.kakao.com":
149
- self.pack_title = self.url.replace("https://e.kakao.com/t/", "")
150
- (
151
- self.author,
152
- title_ko,
153
- thumbnail_urls,
154
- ) = MetadataKakao.get_info_from_pack_title(self.pack_title)
155
-
156
- assert self.author
157
- assert title_ko
158
- assert thumbnail_urls
159
-
160
- if not thumbnail_urls:
215
+ self.pack_title = urlparse(self.url).path.split("/")[-1]
216
+ self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
217
+ self.pack_title
218
+ )
219
+
220
+ if not self.pack_info_unauthed:
161
221
  self.cb.put(
162
222
  "Download failed: Cannot download metadata for sticker pack"
163
223
  )
164
224
  return False
165
225
 
166
- if auth_token:
167
- item_code = MetadataKakao.get_item_code(title_ko, auth_token)
226
+ self.author = self.pack_info_unauthed["result"]["artist"]
227
+ title_ko = self.pack_info_unauthed["result"]["title"]
228
+ thumbnail_urls = self.pack_info_unauthed["result"]["thumbnailUrls"]
229
+
230
+ if self.auth_token:
231
+ item_code = MetadataKakao.get_item_code(title_ko, self.auth_token)
168
232
  if item_code:
169
233
  return self.download_animated(item_code)
170
234
  msg = "Warning: Cannot get item code.\n"
@@ -204,6 +268,115 @@ class DownloadKakao(DownloadBase):
204
268
  self.out_dir, title=self.pack_title, author=self.author
205
269
  )
206
270
 
271
+ success = self.download_animated_zip(item_code)
272
+ if not success:
273
+ self.cb.put("Trying to download one by one")
274
+ success = self.download_animated_files(item_code)
275
+
276
+ return success
277
+
278
+ def download_animated_files(self, item_code: str) -> bool:
279
+ play_exts = [".webp", ".gif", ".png", ""]
280
+ play_types = ["emot", "emoji", ""] # emot = normal; emoji = mini
281
+ play_path_format = None
282
+ sound_exts = [".mp3", ""]
283
+ sound_path_format = None
284
+ stickers_count = 32 # https://emoticonstudio.kakao.com/pages/start
285
+
286
+ if not self.pack_info_authed and self.auth_token:
287
+ self.pack_info_authed = MetadataKakao.get_pack_info_authed(
288
+ item_code, self.auth_token
289
+ )
290
+ if self.pack_info_authed:
291
+ preview_data = self.pack_info_authed["itemUnitInfo"][0]["previewData"]
292
+ play_path_format = preview_data["playPathFormat"]
293
+ sound_path_format = preview_data["soundPathFormat"]
294
+ stickers_count = preview_data["num"]
295
+ else:
296
+ if not self.pack_info_unauthed:
297
+ public_url = None
298
+ if urlparse(self.url).netloc == "emoticon.kakao.com":
299
+ r = requests.get(self.url)
300
+ # Share url would redirect to public url without headers
301
+ public_url = r.url
302
+ elif urlparse(self.url).netloc == "e.kakao.com":
303
+ public_url = self.url
304
+ if public_url:
305
+ pack_title = urlparse(public_url).path.split("/")[-1]
306
+ self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
307
+ pack_title
308
+ )
309
+
310
+ if self.pack_info_unauthed:
311
+ stickers_count = len(self.pack_info_unauthed["result"]["thumbnailUrls"])
312
+
313
+ play_type = ""
314
+ play_ext = ""
315
+ if play_path_format is None:
316
+ for play_type, play_ext in itertools.product(play_types, play_exts):
317
+ r = requests.get(
318
+ f"https://item.kakaocdn.net/dw/{item_code}.{play_type}_001{play_ext}"
319
+ )
320
+ if r.ok:
321
+ break
322
+ if play_ext == "":
323
+ self.cb.put(f"Failed to determine extension of {item_code}")
324
+ return False
325
+ else:
326
+ play_path_format = f"dw/{item_code}.{play_type}_0##{play_ext}"
327
+ else:
328
+ play_ext = "." + play_path_format.split(".")[-1]
329
+
330
+ sound_ext = ""
331
+ if sound_path_format is None:
332
+ for sound_ext in sound_exts:
333
+ r = requests.get(
334
+ f"https://item.kakaocdn.net/dw/{item_code}.sound_001{sound_ext}"
335
+ )
336
+ if r.ok:
337
+ break
338
+ if sound_ext != "":
339
+ sound_path_format = f"dw/{item_code}.sound_0##{sound_ext}"
340
+ elif sound_path_format != "":
341
+ sound_ext = "." + sound_path_format.split(".")[-1]
342
+
343
+ assert play_path_format
344
+ targets: list[tuple[str, Path]] = []
345
+ for num in range(1, stickers_count + 1):
346
+ play_url = "https://item.kakaocdn.net/" + play_path_format.replace(
347
+ "##", str(num).zfill(2)
348
+ )
349
+ play_dl_path = Path(self.out_dir, str(num).zfill(3) + play_ext)
350
+ targets.append((play_url, play_dl_path))
351
+
352
+ if sound_path_format:
353
+ sound_url = "https://item.kakaocdn.net/" + sound_path_format.replace(
354
+ "##", str(num).zfill(2)
355
+ )
356
+ sound_dl_path = Path(self.out_dir, str(num).zfill(3) + sound_ext)
357
+ targets.append((sound_url, sound_dl_path))
358
+
359
+ self.download_multiple_files(targets)
360
+
361
+ for target in targets:
362
+ f_path = target[1]
363
+ ext = Path(f_path).suffix
364
+
365
+ if ext not in (".gif", ".webp"):
366
+ continue
367
+
368
+ with open(f_path, "rb") as f:
369
+ data = f.read()
370
+ data = DecryptKakao.xor_data(data)
371
+ self.cb.put(f"Decrypted {f_path}")
372
+ with open(f_path, "wb+") as f:
373
+ f.write(data)
374
+
375
+ self.cb.put(f"Finished getting {item_code}")
376
+
377
+ return True
378
+
379
+ def download_animated_zip(self, item_code: str) -> bool:
207
380
  pack_url = f"http://item.kakaocdn.net/dw/{item_code}.file_pack.zip"
208
381
 
209
382
  zip_file = self.download_file(pack_url)
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- __version__ = "2.8.12"
3
+ __version__ = "2.8.14"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sticker-convert
3
- Version: 2.8.12
3
+ Version: 2.8.14
4
4
  Summary: Convert (animated) stickers to/from WhatsApp, Telegram, Signal, Line, Kakao, Viber, iMessage. Written in Python.
5
5
  Author-email: laggykiller <chaudominic2@gmail.com>
6
6
  Maintainer-email: laggykiller <chaudominic2@gmail.com>
@@ -377,8 +377,8 @@ Requires-Dist: numpy >=1.22.4
377
377
  Requires-Dist: Pillow ~=10.3.0
378
378
  Requires-Dist: pyoxipng ~=9.0.0
379
379
  Requires-Dist: python-telegram-bot ~=21.2
380
- Requires-Dist: requests ~=2.32.2
381
- Requires-Dist: rlottie-python ~=1.3.4
380
+ Requires-Dist: requests ~=2.32.3
381
+ Requires-Dist: rlottie-python ~=1.3.5
382
382
  Requires-Dist: signalstickers-client-fork-laggykiller ~=3.3.0.post2
383
383
  Requires-Dist: sqlcipher3-wheels ~=0.5.2.post1
384
384
  Requires-Dist: tqdm ~=4.66.4
@@ -6,10 +6,10 @@ sticker_convert/definitions.py,sha256=ZhP2ALCEud-w9ZZD4c3TDG9eHGPZyaAL7zPUsJAbjt
6
6
  sticker_convert/gui.py,sha256=TRPGwMhSMPHnZppHmw2OWHKTJtGoeLpGWD0eRYi4_yk,30707
7
7
  sticker_convert/job.py,sha256=vKv1--y4MVmZV_IBpUhEfNEiUeEqrTR1umzlALPXKdw,25775
8
8
  sticker_convert/job_option.py,sha256=JHAFCxp7-dDwD-1PbpYLAFRF3OoJu8cj_BjOm5r8Gp8,7732
9
- sticker_convert/version.py,sha256=D5g-FbZhwVOSGUdo2Me669Y9p6l08MwfjYV6tsmcD8o,47
9
+ sticker_convert/version.py,sha256=xfnmuNP-PsbPT9tAIe1hZe8BVTotwNbxoJXlFmvcuP4,47
10
10
  sticker_convert/downloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  sticker_convert/downloaders/download_base.py,sha256=x18bI2mPpbXRnSmStBHEb1IvN-VPCilOHLQUs6YPUEU,4041
12
- sticker_convert/downloaders/download_kakao.py,sha256=UFp7EpMea62fIePY5DfhH4jThAwdeazfoC5iW1g4dAo,8516
12
+ sticker_convert/downloaders/download_kakao.py,sha256=hHilDDzYaGU5BQmOwMF0NKnsysxh8loybfoFSNQQdSw,14955
13
13
  sticker_convert/downloaders/download_line.py,sha256=9WzOWujTbZdAqBi52k21OUEfRmcV1loCaJiDmg6dklw,17853
14
14
  sticker_convert/downloaders/download_signal.py,sha256=PfwscdbcEd_5C3Ecs0F8Qc8si1sLzLodAdnsHVwXgac,3063
15
15
  sticker_convert/downloaders/download_telegram.py,sha256=jufMqc78aXOPDr7fQf9ykkNyhQ7KVCp4gRBxs09NgMo,4614
@@ -93,9 +93,9 @@ sticker_convert/utils/media/apple_png_normalize.py,sha256=LbrQhc7LlYX4I9ek4XJsZE
93
93
  sticker_convert/utils/media/codec_info.py,sha256=1QfW3wgZ5vOk7T4XtLHYvJK1x8RbASRPSvhKEPkcu9A,15747
94
94
  sticker_convert/utils/media/decrypt_kakao.py,sha256=4wq9ZDRnFkx1WmFZnyEogBofiLGsWQM_X69HlA36578,1947
95
95
  sticker_convert/utils/media/format_verify.py,sha256=Xf94jyqk_6M9IlFGMy0wYIgQKn_yg00nD4XW0CgAbew,5732
96
- sticker_convert-2.8.12.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
97
- sticker_convert-2.8.12.dist-info/METADATA,sha256=fch26QKaCx3jmBCTTfyE1PV5rBPcctP5xA_94gOrl0U,50376
98
- sticker_convert-2.8.12.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
99
- sticker_convert-2.8.12.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
100
- sticker_convert-2.8.12.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
101
- sticker_convert-2.8.12.dist-info/RECORD,,
96
+ sticker_convert-2.8.14.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
97
+ sticker_convert-2.8.14.dist-info/METADATA,sha256=oMOCYbDpT4foe2pN0gsrfAAG1DASm8ak3uUjc9tpCpA,50376
98
+ sticker_convert-2.8.14.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
99
+ sticker_convert-2.8.14.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
100
+ sticker_convert-2.8.14.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
101
+ sticker_convert-2.8.14.dist-info/RECORD,,