sticker-convert 2.8.11__py3-none-any.whl → 2.8.13__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,13 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import annotations
3
3
 
4
+ import itertools
4
5
  import json
5
6
  import zipfile
6
7
  from io import BytesIO
7
8
  from pathlib import Path
8
- from typing import Any, List, Optional, Tuple
9
+ from typing import Any, List, Optional, Tuple, cast
9
10
  from urllib.parse import urlparse
10
11
 
12
+ import js2py # type: ignore
11
13
  import requests
12
14
  from bs4 import BeautifulSoup
13
15
  from bs4.element import Tag
@@ -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,35 +58,34 @@ 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:
61
+ item_code_fake = cast(str, app_scheme_link_tag["data-i"])
62
+
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:
41
69
  return None, None
42
- if isinstance(data_urls, list):
43
- data_url = data_urls[0]
44
- else:
45
- data_url = data_urls
46
70
 
47
- item_code = data_url.replace("kakaotalk://store/emoticon/", "").split("?")[0]
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[: func_end_pos + 1]
76
+ web2app_start_pos = js.find("daumtools.web2app(")
77
+ js = js[:web2app_start_pos] + "return a;}"
78
+ get_item_code = js2py.eval_js(js) # type: ignore
79
+ kakao_scheme_link = cast(
80
+ str,
81
+ get_item_code(
82
+ "kakaotalk://store/emoticon/${i}?referer=share_link", item_code_fake
83
+ ),
84
+ )
85
+ item_code = urlparse(kakao_scheme_link).path.split("/")[-1]
48
86
 
49
87
  return pack_title, item_code
50
88
 
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}")
56
-
57
- if pack_meta_r.status_code == 200:
58
- pack_meta = json.loads(pack_meta_r.text)
59
- else:
60
- return None, None, None
61
-
62
- author = pack_meta["result"]["artist"]
63
- title_ko = pack_meta["result"]["title"]
64
- thumbnail_urls = pack_meta["result"]["thumbnailUrls"]
65
-
66
- return author, title_ko, thumbnail_urls
67
-
68
89
  @staticmethod
69
90
  def get_item_code(title_ko: str, auth_token: str) -> Optional[str]:
70
91
  headers = {
@@ -88,9 +109,27 @@ class MetadataKakao:
88
109
  return item_code
89
110
 
90
111
  @staticmethod
91
- def get_title_from_id(item_code: str, auth_token: str) -> Optional[str]:
112
+ def get_pack_info_unauthed(
113
+ pack_title: str,
114
+ ) -> Optional[dict[str, Any]]:
115
+ pack_meta_r = requests.get(f"https://e.kakao.com/api/v1/items/t/{pack_title}")
116
+
117
+ if pack_meta_r.status_code == 200:
118
+ pack_meta = json.loads(pack_meta_r.text)
119
+ else:
120
+ return None
121
+
122
+ return pack_meta
123
+
124
+ @staticmethod
125
+ def get_pack_info_authed(
126
+ item_code: str, auth_token: str
127
+ ) -> Optional[dict[str, Any]]:
92
128
  headers = {
93
129
  "Authorization": auth_token,
130
+ "Talk-Agent": "android/10.8.1",
131
+ "Talk-Language": "en",
132
+ "User-Agent": "okhttp/4.10.0",
94
133
  }
95
134
 
96
135
  response = requests.post(
@@ -102,11 +141,8 @@ class MetadataKakao:
102
141
  return None
103
142
 
104
143
  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
144
 
109
- return title
145
+ return response_json
110
146
 
111
147
 
112
148
  class DownloadKakao(DownloadBase):
@@ -114,11 +150,15 @@ class DownloadKakao(DownloadBase):
114
150
  super().__init__(*args, **kwargs)
115
151
  self.pack_title: Optional[str] = None
116
152
  self.author: Optional[str] = None
153
+ self.auth_token: Optional[str] = None
154
+
155
+ self.pack_info_unauthed: Optional[dict[str, Any]] = None
156
+ self.pack_info_authed: Optional[dict[str, Any]] = None
117
157
 
118
158
  def download_stickers_kakao(self) -> bool:
119
- auth_token = None
159
+ self.auth_token = None
120
160
  if self.opt_cred:
121
- auth_token = self.opt_cred.kakao_auth_token
161
+ self.auth_token = self.opt_cred.kakao_auth_token
122
162
 
123
163
  if urlparse(self.url).netloc == "emoticon.kakao.com":
124
164
  self.pack_title, item_code = MetadataKakao.get_info_from_share_link(
@@ -134,9 +174,13 @@ class DownloadKakao(DownloadBase):
134
174
  item_code = self.url.replace("kakaotalk://store/emoticon/", "")
135
175
 
136
176
  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:
177
+ if self.auth_token:
178
+ self.pack_info_authed = MetadataKakao.get_pack_info_authed(
179
+ item_code, self.auth_token
180
+ )
181
+ if self.pack_info_authed:
182
+ self.pack_title = self.pack_info_authed["itemUnitInfo"][0]["title"]
183
+ else:
140
184
  self.cb.put("Warning: Cannot get pack_title with auth_token.")
141
185
  self.cb.put(
142
186
  "Is auth_token invalid / expired? Try to regenerate it."
@@ -146,25 +190,23 @@ class DownloadKakao(DownloadBase):
146
190
  return self.download_animated(item_code)
147
191
 
148
192
  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:
193
+ self.pack_title = urlparse(self.url).path.split("/")[-1]
194
+ self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
195
+ self.pack_title
196
+ )
197
+
198
+ if not self.pack_info_unauthed:
161
199
  self.cb.put(
162
200
  "Download failed: Cannot download metadata for sticker pack"
163
201
  )
164
202
  return False
165
203
 
166
- if auth_token:
167
- item_code = MetadataKakao.get_item_code(title_ko, auth_token)
204
+ self.author = self.pack_info_unauthed["result"]["artist"]
205
+ title_ko = self.pack_info_unauthed["result"]["title"]
206
+ thumbnail_urls = self.pack_info_unauthed["result"]["thumbnailUrls"]
207
+
208
+ if self.auth_token:
209
+ item_code = MetadataKakao.get_item_code(title_ko, self.auth_token)
168
210
  if item_code:
169
211
  return self.download_animated(item_code)
170
212
  msg = "Warning: Cannot get item code.\n"
@@ -204,6 +246,115 @@ class DownloadKakao(DownloadBase):
204
246
  self.out_dir, title=self.pack_title, author=self.author
205
247
  )
206
248
 
249
+ success = self.download_animated_zip(item_code)
250
+ if not success:
251
+ self.cb.put("Trying to download one by one")
252
+ success = self.download_animated_files(item_code)
253
+
254
+ return success
255
+
256
+ def download_animated_files(self, item_code: str) -> bool:
257
+ play_exts = [".webp", ".gif", ".png", ""]
258
+ play_types = ["emot", "emoji", ""] # emot = normal; emoji = mini
259
+ play_path_format = None
260
+ sound_exts = [".mp3", ""]
261
+ sound_path_format = None
262
+ stickers_count = 32 # https://emoticonstudio.kakao.com/pages/start
263
+
264
+ if not self.pack_info_authed and self.auth_token:
265
+ self.pack_info_authed = MetadataKakao.get_pack_info_authed(
266
+ item_code, self.auth_token
267
+ )
268
+ if self.pack_info_authed:
269
+ preview_data = self.pack_info_authed["itemUnitInfo"][0]["previewData"]
270
+ play_path_format = preview_data["playPathFormat"]
271
+ sound_path_format = preview_data["soundPathFormat"]
272
+ stickers_count = preview_data["num"]
273
+ else:
274
+ if not self.pack_info_unauthed:
275
+ public_url = None
276
+ if urlparse(self.url).netloc == "emoticon.kakao.com":
277
+ r = requests.get(self.url)
278
+ # Share url would redirect to public url without headers
279
+ public_url = r.url
280
+ elif urlparse(self.url).netloc == "e.kakao.com":
281
+ public_url = self.url
282
+ if public_url:
283
+ pack_title = urlparse(public_url).path.split("/")[-1]
284
+ self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
285
+ pack_title
286
+ )
287
+
288
+ if self.pack_info_unauthed:
289
+ stickers_count = len(self.pack_info_unauthed["result"]["thumbnailUrls"])
290
+
291
+ play_type = ""
292
+ play_ext = ""
293
+ if play_path_format is None:
294
+ for play_type, play_ext in itertools.product(play_types, play_exts):
295
+ r = requests.get(
296
+ f"https://item.kakaocdn.net/dw/{item_code}.{play_type}_001{play_ext}"
297
+ )
298
+ if r.ok:
299
+ break
300
+ if play_ext == "":
301
+ self.cb.put(f"Failed to determine extension of {item_code}")
302
+ return False
303
+ else:
304
+ play_path_format = f"dw/{item_code}.{play_type}_0##{play_ext}"
305
+ else:
306
+ play_ext = "." + play_path_format.split(".")[-1]
307
+
308
+ sound_ext = ""
309
+ if sound_path_format is None:
310
+ for sound_ext in sound_exts:
311
+ r = requests.get(
312
+ f"https://item.kakaocdn.net/dw/{item_code}.sound_001{sound_ext}"
313
+ )
314
+ if r.ok:
315
+ break
316
+ if sound_ext != "":
317
+ sound_path_format = f"dw/{item_code}.sound_0##{sound_ext}"
318
+ elif sound_path_format != "":
319
+ sound_ext = "." + sound_path_format.split(".")[-1]
320
+
321
+ assert play_path_format
322
+ targets: list[tuple[str, Path]] = []
323
+ for num in range(1, stickers_count + 1):
324
+ play_url = "https://item.kakaocdn.net/" + play_path_format.replace(
325
+ "##", str(num).zfill(2)
326
+ )
327
+ play_dl_path = Path(self.out_dir, str(num).zfill(3) + play_ext)
328
+ targets.append((play_url, play_dl_path))
329
+
330
+ if sound_path_format:
331
+ sound_url = "https://item.kakaocdn.net/" + sound_path_format.replace(
332
+ "##", str(num).zfill(2)
333
+ )
334
+ sound_dl_path = Path(self.out_dir, str(num).zfill(3) + sound_ext)
335
+ targets.append((sound_url, sound_dl_path))
336
+
337
+ self.download_multiple_files(targets)
338
+
339
+ for target in targets:
340
+ f_path = target[1]
341
+ ext = Path(f_path).suffix
342
+
343
+ if ext not in (".gif", ".webp"):
344
+ continue
345
+
346
+ with open(f_path, "rb") as f:
347
+ data = f.read()
348
+ data = DecryptKakao.xor_data(data)
349
+ self.cb.put(f"Decrypted {f_path}")
350
+ with open(f_path, "wb+") as f:
351
+ f.write(data)
352
+
353
+ self.cb.put(f"Finished getting {item_code}")
354
+
355
+ return True
356
+
357
+ def download_animated_zip(self, item_code: str) -> bool:
207
358
  pack_url = f"http://item.kakaocdn.net/dw/{item_code}.file_pack.zip"
208
359
 
209
360
  zip_file = self.download_file(pack_url)
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- __version__ = "2.8.11"
3
+ __version__ = "2.8.13"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sticker-convert
3
- Version: 2.8.11
3
+ Version: 2.8.13
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>
@@ -371,15 +371,16 @@ Requires-Dist: av ~=12.1.0
371
371
  Requires-Dist: beautifulsoup4 ~=4.12.3
372
372
  Requires-Dist: rookiepy ~=0.5.1
373
373
  Requires-Dist: imagequant ~=1.1.1
374
+ Requires-Dist: js2py ~=0.74
374
375
  Requires-Dist: memory-tempfile ~=2.2.3
375
376
  Requires-Dist: mergedeep ~=1.3.4
376
377
  Requires-Dist: numpy >=1.22.4
377
378
  Requires-Dist: Pillow ~=10.3.0
378
379
  Requires-Dist: pyoxipng ~=9.0.0
379
380
  Requires-Dist: python-telegram-bot ~=21.2
380
- Requires-Dist: requests ~=2.32.2
381
- Requires-Dist: rlottie-python ~=1.3.4
382
- Requires-Dist: signalstickers-client-fork-laggykiller ~=3.3.0.post1
381
+ Requires-Dist: requests ~=2.32.3
382
+ Requires-Dist: rlottie-python ~=1.3.5
383
+ Requires-Dist: signalstickers-client-fork-laggykiller ~=3.3.0.post2
383
384
  Requires-Dist: sqlcipher3-wheels ~=0.5.2.post1
384
385
  Requires-Dist: tqdm ~=4.66.4
385
386
  Requires-Dist: ttkbootstrap-fork-laggykiller ~=1.5.1
@@ -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=oxsRvN-1re7LUAQn0GzovVDlajKtGywZotOzD3vzeIE,47
9
+ sticker_convert/version.py,sha256=9-pwqpZSBU0RJFweDIiL0yO5EXYfWSFjeU-Fw26RIA4,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=d0950G_Nqpb9cCWgVKVJBtSNFSykv5zrye0uqHAGenY,14193
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.11.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
97
- sticker_convert-2.8.11.dist-info/METADATA,sha256=yeRKxmheS7CvSGvQIwZ8-uqxUs7NAL6Ammf3KKBQCYo,50376
98
- sticker_convert-2.8.11.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
99
- sticker_convert-2.8.11.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
100
- sticker_convert-2.8.11.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
101
- sticker_convert-2.8.11.dist-info/RECORD,,
96
+ sticker_convert-2.8.13.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
97
+ sticker_convert-2.8.13.dist-info/METADATA,sha256=7GSocwPMQW299PlQZ9BHiA5LGu-Je1_UfMyh3uBbZ-Q,50404
98
+ sticker_convert-2.8.13.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
99
+ sticker_convert-2.8.13.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
100
+ sticker_convert-2.8.13.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
101
+ sticker_convert-2.8.13.dist-info/RECORD,,