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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,