sticker-convert 2.8.11__py3-none-any.whl → 2.8.13__py3-none-any.whl

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