apksearch 1.3.1__tar.gz → 1.3.5__tar.gz
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.
- {apksearch-1.3.1 → apksearch-1.3.5}/PKG-INFO +3 -1
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/cli.py +53 -29
- apksearch-1.3.5/apksearch/sites/__init__.py +10 -0
- apksearch-1.3.5/apksearch/sites/apkad.py +142 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/sites/apkcombo.py +4 -3
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/sites/apkfab.py +6 -2
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/sites/apkmirror.py +6 -3
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/sites/apkpure.py +4 -2
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/sites/appteka.py +4 -2
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/sites/aptoide.py +3 -1
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch.egg-info/PKG-INFO +3 -1
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch.egg-info/requires.txt +3 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/pyproject.toml +2 -1
- {apksearch-1.3.1 → apksearch-1.3.5}/tests/test_apkfab.py +12 -12
- {apksearch-1.3.1 → apksearch-1.3.5}/tests/test_apkmirror.py +7 -5
- {apksearch-1.3.1 → apksearch-1.3.5}/tests/test_apkpure.py +12 -12
- {apksearch-1.3.1 → apksearch-1.3.5}/tests/test_aptoide.py +3 -3
- apksearch-1.3.1/apksearch/sites/__init__.py +0 -0
- apksearch-1.3.1/apksearch/sites/apkad.py +0 -109
- {apksearch-1.3.1 → apksearch-1.3.5}/LICENSE +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/README.md +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/__init__.py +4 -4
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch/__main__.py +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch.egg-info/SOURCES.txt +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch.egg-info/dependency_links.txt +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch.egg-info/entry_points.txt +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/apksearch.egg-info/top_level.txt +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/setup.cfg +0 -0
- {apksearch-1.3.1 → apksearch-1.3.5}/tests/test_appteka.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: apksearch
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.5
|
4
4
|
Summary: Search for apks on varius websites
|
5
5
|
Author-email: Abhi <allinoneallinone00@gmail.com>
|
6
6
|
License: MIT License
|
@@ -49,6 +49,8 @@ Provides-Extra: dev
|
|
49
49
|
Requires-Dist: pytest>=7.4.3; extra == "dev"
|
50
50
|
Requires-Dist: black>=23.12.1; extra == "dev"
|
51
51
|
Requires-Dist: flake8>=6.1.0; extra == "dev"
|
52
|
+
Provides-Extra: curl
|
53
|
+
Requires-Dist: curl_cffi>=0.12.1b1; extra == "curl"
|
52
54
|
Dynamic: license-file
|
53
55
|
|
54
56
|
<h1 align="center">apksearch</h1>
|
@@ -1,16 +1,17 @@
|
|
1
1
|
import argparse
|
2
|
-
|
3
2
|
from collections.abc import Callable
|
3
|
+
|
4
|
+
from requests.exceptions import ConnectionError, ConnectTimeout
|
5
|
+
|
4
6
|
from apksearch import (
|
5
|
-
|
6
|
-
APKMirror,
|
7
|
-
AppTeka,
|
7
|
+
APKad,
|
8
8
|
APKCombo,
|
9
9
|
APKFab,
|
10
|
-
|
10
|
+
APKMirror,
|
11
|
+
APKPure,
|
12
|
+
AppTeka,
|
11
13
|
Aptoide,
|
12
14
|
)
|
13
|
-
from requests.exceptions import ConnectionError, ConnectTimeout
|
14
15
|
|
15
16
|
# Color codes
|
16
17
|
BOLD = "\033[1m"
|
@@ -21,7 +22,7 @@ NC = "\033[0m"
|
|
21
22
|
|
22
23
|
|
23
24
|
def search(
|
24
|
-
func: Callable[[str, str], object],
|
25
|
+
func: Callable[[str, str | None], object],
|
25
26
|
pkg_name: str,
|
26
27
|
version: str | None,
|
27
28
|
log_err: bool = False,
|
@@ -30,7 +31,8 @@ def search(
|
|
30
31
|
func(pkg_name, version)
|
31
32
|
except Exception as exc:
|
32
33
|
if log_err:
|
33
|
-
|
34
|
+
func_name = getattr(func, "__name__", func.__class__.__name__)
|
35
|
+
print(f"Error in {func_name}: {exc}")
|
34
36
|
else:
|
35
37
|
pass
|
36
38
|
|
@@ -90,16 +92,20 @@ def search_apkfab(pkg_name: str, version: str | None) -> None:
|
|
90
92
|
def search_apkad(pkg_name: str, version: str | None) -> None:
|
91
93
|
apkad = APKad(pkg_name)
|
92
94
|
try:
|
93
|
-
result_apkad: tuple[str, str] | None = apkad.search_apk()
|
95
|
+
result_apkad: tuple[str, list[tuple[str, str]]] | None = apkad.search_apk()
|
94
96
|
except (ConnectionError, ConnectTimeout):
|
95
97
|
result_apkad = None
|
96
98
|
print(f"{RED}Failed to resolve 'api.apk.ad'!{NC}")
|
97
99
|
if result_apkad:
|
98
|
-
title,
|
100
|
+
title, apk_links = result_apkad
|
99
101
|
print(f"{BOLD}APKAD:{NC} Found {GREEN}{title}{NC}") if title else None
|
100
|
-
|
101
|
-
f" ╰─> {BOLD}
|
102
|
-
|
102
|
+
if not version:
|
103
|
+
print(f" ╰─> {BOLD}Available APKs:{NC}")
|
104
|
+
for i, (filename, url) in enumerate(apk_links, 1):
|
105
|
+
print(f" {i}. {filename}")
|
106
|
+
print(f" ╰─> {BOLD}Download:{NC} {YELLOW}{url}{NC}")
|
107
|
+
else:
|
108
|
+
print(" ╰─> Doesn't support version search!")
|
103
109
|
else:
|
104
110
|
print(f"{BOLD}APKAD:{NC} No Results!")
|
105
111
|
|
@@ -141,7 +147,7 @@ def search_apkmirror(pkg_name: str, version: str | None) -> None:
|
|
141
147
|
print(f"{BOLD}APKMirror:{NC} Found {GREEN}{title}{NC}") if title else None
|
142
148
|
print(f" ╰─> {BOLD}Link: {YELLOW}{apk_link}{NC}") if not version else None
|
143
149
|
if version:
|
144
|
-
download_link = apkmirror.find_version(apk_link, version)
|
150
|
+
download_link = apkmirror.find_version(apk_link, version, title)
|
145
151
|
if download_link:
|
146
152
|
print(
|
147
153
|
f" ╰─> {BOLD}Version: {GREEN}{version}{NC} - {YELLOW}{download_link}{NC}"
|
@@ -155,7 +161,7 @@ def search_apkmirror(pkg_name: str, version: str | None) -> None:
|
|
155
161
|
def search_appteka(pkg_name: str, version: str | None) -> None:
|
156
162
|
appteka = AppTeka(pkg_name)
|
157
163
|
try:
|
158
|
-
result_appteka: tuple[str, str] | None = appteka.search_apk(version)
|
164
|
+
result_appteka: tuple[str, str | None] | None = appteka.search_apk(version)
|
159
165
|
except (ConnectionError, ConnectTimeout):
|
160
166
|
result_appteka = None
|
161
167
|
print(f"{RED}Failed to resolve 'appteka.store'!{NC}")
|
@@ -208,26 +214,44 @@ def main():
|
|
208
214
|
parser.add_argument(
|
209
215
|
"--log_err", help="Enable error logs", action="store_true", required=False
|
210
216
|
)
|
217
|
+
parser.add_argument(
|
218
|
+
"--sites",
|
219
|
+
nargs="+",
|
220
|
+
choices=[
|
221
|
+
"apkpure",
|
222
|
+
"apkmirror",
|
223
|
+
"aptoide",
|
224
|
+
"appteka",
|
225
|
+
"apkcombo",
|
226
|
+
"apkfab",
|
227
|
+
"apkad",
|
228
|
+
],
|
229
|
+
help="Specify the sites to search on",
|
230
|
+
required=False,
|
231
|
+
)
|
211
232
|
args = parser.parse_args()
|
212
233
|
|
213
234
|
pkg_name = args.pkg_name
|
214
235
|
version = args.version
|
215
236
|
log_err = args.log_err
|
237
|
+
sites = args.sites
|
238
|
+
|
216
239
|
print(f"{BOLD}Searching for {YELLOW}{pkg_name}{NC}...")
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
240
|
+
|
241
|
+
if not sites or "apkpure" in sites:
|
242
|
+
search(search_apkpure, pkg_name, version, log_err)
|
243
|
+
if not sites or "apkmirror" in sites:
|
244
|
+
search(search_apkmirror, pkg_name, version, log_err)
|
245
|
+
if not sites or "aptoide" in sites:
|
246
|
+
search(search_aptoide, pkg_name, version, log_err)
|
247
|
+
if not sites or "appteka" in sites:
|
248
|
+
search(search_appteka, pkg_name, version, log_err)
|
249
|
+
if not sites or "apkcombo" in sites:
|
250
|
+
search(search_apkcombo, pkg_name, version, log_err)
|
251
|
+
if not sites or "apkfab" in sites:
|
252
|
+
search(search_apkfab, pkg_name, version, log_err)
|
253
|
+
if not sites or "apkad" in sites:
|
254
|
+
search(search_apkad, pkg_name, version, log_err)
|
231
255
|
|
232
256
|
|
233
257
|
if __name__ == "__main__":
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import base64
|
2
|
+
import json
|
3
|
+
|
4
|
+
from bs4 import BeautifulSoup
|
5
|
+
|
6
|
+
from apksearch.sites import requests
|
7
|
+
|
8
|
+
|
9
|
+
class APKad:
|
10
|
+
"""
|
11
|
+
This class provides methods to search for an APK on APKAD based on package name,
|
12
|
+
and to find available versions and their download links for a given APK link.
|
13
|
+
|
14
|
+
Parameters:
|
15
|
+
pkg_name (str): The package name of the APK to search for.
|
16
|
+
|
17
|
+
Attributes:
|
18
|
+
pkg_name (str): The package name of the APK to search for.
|
19
|
+
base_url (str): The base URL of the APKAD website.
|
20
|
+
search_url (str): The URL used to search for APKs on APKAD.
|
21
|
+
headers (dict): The headers used for making HTTP requests.
|
22
|
+
session (requests.Session): The session object used for making HTTP requests.
|
23
|
+
|
24
|
+
Methods:
|
25
|
+
search_apk() -> None | tuple[str, str]:
|
26
|
+
Searches for the APK on APKAD and returns the title and link if found.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, pkg_name: str):
|
30
|
+
self.pkg_name = pkg_name
|
31
|
+
self.base_url = "https://apkdownloader.pages.dev"
|
32
|
+
self.api_url = "https://api.mi9.com"
|
33
|
+
self.token_url = "https://token.mi9.com/"
|
34
|
+
self.search_url = self.api_url + "/get"
|
35
|
+
self.headers = {
|
36
|
+
"accept": "text/event-stream",
|
37
|
+
"accept-language": "en-US,en;q=0.9,en-IN;q=0.8",
|
38
|
+
"cache-control": "no-cache",
|
39
|
+
"dnt": "1",
|
40
|
+
"origin": "https://apkdownloader.pages.dev",
|
41
|
+
"pragma": "no-cache",
|
42
|
+
"priority": "u=1, i",
|
43
|
+
"referer": "https://apkdownloader.pages.dev/",
|
44
|
+
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
45
|
+
"sec-ch-ua-mobile": "?0",
|
46
|
+
"sec-ch-ua-platform": '"Windows"',
|
47
|
+
"sec-fetch-dest": "empty",
|
48
|
+
"sec-fetch-mode": "cors",
|
49
|
+
"sec-fetch-site": "same-site",
|
50
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
51
|
+
}
|
52
|
+
self.session = requests.Session()
|
53
|
+
|
54
|
+
def get_token(self) -> tuple[str, int] | None:
|
55
|
+
"""
|
56
|
+
Retrieves a token from the token endpoint.
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
tuple[str, int]: A tuple containing the token and timestamp if successful.
|
60
|
+
None: If the token retrieval fails.
|
61
|
+
"""
|
62
|
+
data = {
|
63
|
+
"package": self.pkg_name,
|
64
|
+
"device": "phone",
|
65
|
+
"arch": "arm64-v8a",
|
66
|
+
"vc": "",
|
67
|
+
"device_id": "",
|
68
|
+
"sdk": "default",
|
69
|
+
}
|
70
|
+
response = self.session.post(self.token_url, headers=self.headers, json=data)
|
71
|
+
if response.status_code == 200:
|
72
|
+
token_data = response.json()
|
73
|
+
if token_data.get("success"):
|
74
|
+
return token_data.get("token"), token_data.get("timestamp")
|
75
|
+
return None
|
76
|
+
|
77
|
+
def search_apk(self) -> None | tuple[str, list[tuple[str, str]]]:
|
78
|
+
token_tuple = self.get_token()
|
79
|
+
if not token_tuple:
|
80
|
+
return None
|
81
|
+
|
82
|
+
token, ts = token_tuple
|
83
|
+
data_json = json.dumps(
|
84
|
+
{
|
85
|
+
"hl": "en",
|
86
|
+
"package": self.pkg_name,
|
87
|
+
"device": "phone",
|
88
|
+
"arch": "arm64-v8a",
|
89
|
+
"vc": "",
|
90
|
+
"device_id": "",
|
91
|
+
"sdk": "default",
|
92
|
+
"timestamp": ts,
|
93
|
+
},
|
94
|
+
separators=(",", ":"),
|
95
|
+
)
|
96
|
+
|
97
|
+
data_b64 = base64.b64encode(data_json.encode("utf-8")).decode("utf-8")
|
98
|
+
|
99
|
+
params = {"token": token, "data": data_b64}
|
100
|
+
|
101
|
+
response = self.session.get(
|
102
|
+
self.search_url, headers=self.headers, params=params, stream=True
|
103
|
+
)
|
104
|
+
|
105
|
+
stream_response = None
|
106
|
+
for line in response.iter_lines():
|
107
|
+
if not line:
|
108
|
+
continue
|
109
|
+
line_response = line.decode("utf-8")
|
110
|
+
if line_response.startswith("data: "):
|
111
|
+
payload = line_response[6:]
|
112
|
+
try:
|
113
|
+
j = json.loads(payload)
|
114
|
+
if j.get("progress") == 100 and j.get("html"):
|
115
|
+
stream_response = j
|
116
|
+
break
|
117
|
+
except json.JSONDecodeError:
|
118
|
+
continue
|
119
|
+
|
120
|
+
if stream_response:
|
121
|
+
html_body = stream_response["html"]
|
122
|
+
soup = BeautifulSoup(html_body, "html.parser")
|
123
|
+
if not soup:
|
124
|
+
return None
|
125
|
+
|
126
|
+
title = soup.find("li", {"class": "_title"})
|
127
|
+
title = title.text.strip() if title else self.pkg_name
|
128
|
+
|
129
|
+
apk_files_div = soup.find("div", {"id": "apkslist"})
|
130
|
+
if not apk_files_div:
|
131
|
+
return None
|
132
|
+
|
133
|
+
apk_links: list[tuple[str, str]] = []
|
134
|
+
for a in apk_files_div.find_all("a", href=True):
|
135
|
+
link = a["href"].strip()
|
136
|
+
filename = a.find("span", {"class": "der_name"})
|
137
|
+
filename = filename.text.strip() if filename else link.split("/")[-1]
|
138
|
+
apk_links.append((filename, link))
|
139
|
+
|
140
|
+
return title, apk_links
|
141
|
+
|
142
|
+
return None
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from bs4 import BeautifulSoup
|
2
|
-
|
2
|
+
|
3
|
+
from apksearch.sites import requests
|
3
4
|
|
4
5
|
|
5
6
|
class APKCombo:
|
@@ -27,7 +28,7 @@ class APKCombo:
|
|
27
28
|
|
28
29
|
def __init__(self, pkg_name: str):
|
29
30
|
self.pkg_name = pkg_name
|
30
|
-
self.base_url = "https://apkcombo.
|
31
|
+
self.base_url = "https://apkcombo.com"
|
31
32
|
self.search_url = self.base_url + "/search"
|
32
33
|
self.headers = {
|
33
34
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
@@ -37,7 +38,7 @@ class APKCombo:
|
|
37
38
|
"dnt": "1",
|
38
39
|
"pragma": "no-cache",
|
39
40
|
"priority": "u=0, i",
|
40
|
-
"referer": "https://apkcombo.
|
41
|
+
"referer": "https://apkcombo.com/",
|
41
42
|
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
42
43
|
"sec-ch-ua-mobile": "?0",
|
43
44
|
"sec-ch-ua-platform": '"Windows"',
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from bs4 import BeautifulSoup
|
2
|
-
|
2
|
+
|
3
|
+
from apksearch.sites import requests, curl
|
3
4
|
|
4
5
|
|
5
6
|
class APKFab:
|
@@ -59,7 +60,10 @@ class APKFab:
|
|
59
60
|
"""
|
60
61
|
pkg_name = self.pkg_name
|
61
62
|
url = self.search_url + pkg_name
|
62
|
-
|
63
|
+
params = {"headers": self.headers}
|
64
|
+
if curl:
|
65
|
+
params["impersonate"] = "chrome"
|
66
|
+
response: requests.Response = self.session.get(url, **params)
|
63
67
|
soup = BeautifulSoup(response.text, "html.parser")
|
64
68
|
search_result = soup.find("div", {"class": "search-white"})
|
65
69
|
if search_result:
|
@@ -1,4 +1,6 @@
|
|
1
|
-
import
|
1
|
+
import re
|
2
|
+
|
3
|
+
from apksearch.sites import requests
|
2
4
|
|
3
5
|
|
4
6
|
class APKMirror:
|
@@ -59,7 +61,7 @@ class APKMirror:
|
|
59
61
|
return title, apk_link
|
60
62
|
return None
|
61
63
|
|
62
|
-
def find_version(self, apk_link: str, version: str) -> str:
|
64
|
+
def find_version(self, apk_link: str, version: str, title: str) -> str | None:
|
63
65
|
"""
|
64
66
|
Finds and returns the download link for the given APK link and version.
|
65
67
|
|
@@ -70,7 +72,8 @@ class APKMirror:
|
|
70
72
|
Returns:
|
71
73
|
str: The download link for the specified version of the APK.
|
72
74
|
"""
|
73
|
-
name =
|
75
|
+
name = re.sub(r"[(),]", "", title).lower().replace(" ", "-")
|
76
|
+
name = re.sub(r"-+", "-", name)
|
74
77
|
version = version.replace(".", "-")
|
75
78
|
url = apk_link + name + "-" + version + "-release"
|
76
79
|
response = self.session.get(url, headers=self.headers)
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import re
|
2
|
+
|
2
3
|
from bs4 import BeautifulSoup
|
3
|
-
|
4
|
+
|
5
|
+
from apksearch.sites import requests
|
4
6
|
|
5
7
|
|
6
8
|
class APKPure:
|
@@ -150,7 +152,7 @@ class APKPure:
|
|
150
152
|
title, versions = api_result
|
151
153
|
if versions:
|
152
154
|
return title, versions[0][1]
|
153
|
-
|
155
|
+
|
154
156
|
return None
|
155
157
|
|
156
158
|
def find_versions(self, apk_link: str) -> list[tuple[str, str]]:
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import re
|
2
|
+
|
2
3
|
from bs4 import BeautifulSoup
|
3
|
-
|
4
|
+
|
5
|
+
from apksearch.sites import requests
|
4
6
|
|
5
7
|
|
6
8
|
class AppTeka:
|
@@ -48,7 +50,7 @@ class AppTeka:
|
|
48
50
|
}
|
49
51
|
self.session = requests.Session()
|
50
52
|
|
51
|
-
def search_apk(self, version: str = None) -> None | tuple[str, str]:
|
53
|
+
def search_apk(self, version: str | None = None) -> None | tuple[str, str | None]:
|
52
54
|
"""
|
53
55
|
Searches for the APK on AppTeka and returns the title and link if found.
|
54
56
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: apksearch
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.5
|
4
4
|
Summary: Search for apks on varius websites
|
5
5
|
Author-email: Abhi <allinoneallinone00@gmail.com>
|
6
6
|
License: MIT License
|
@@ -49,6 +49,8 @@ Provides-Extra: dev
|
|
49
49
|
Requires-Dist: pytest>=7.4.3; extra == "dev"
|
50
50
|
Requires-Dist: black>=23.12.1; extra == "dev"
|
51
51
|
Requires-Dist: flake8>=6.1.0; extra == "dev"
|
52
|
+
Provides-Extra: curl
|
53
|
+
Requires-Dist: curl_cffi>=0.12.1b1; extra == "curl"
|
52
54
|
Dynamic: license-file
|
53
55
|
|
54
56
|
<h1 align="center">apksearch</h1>
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "apksearch"
|
7
|
-
version = "1.3.
|
7
|
+
version = "1.3.5"
|
8
8
|
description = "Search for apks on varius websites"
|
9
9
|
authors = [{ name = "Abhi", email = "allinoneallinone00@gmail.com" }]
|
10
10
|
license = { file = "LICENSE" }
|
@@ -34,6 +34,7 @@ issues = "https://github.com/AbhiTheModder/apksearch.git/issues"
|
|
34
34
|
|
35
35
|
[project.optional-dependencies]
|
36
36
|
dev = ["pytest>=7.4.3", "black>=23.12.1", "flake8>=6.1.0"]
|
37
|
+
curl = ["curl_cffi>=0.12.1b1"]
|
37
38
|
|
38
39
|
[tool.pytest.ini_options]
|
39
40
|
minversion = "7.4.3"
|
@@ -24,15 +24,15 @@ def test_find_versions():
|
|
24
24
|
|
25
25
|
assert isinstance(versions, list), "Versions should be a list."
|
26
26
|
assert len(versions) > 0, "No versions found."
|
27
|
-
assert all(
|
28
|
-
|
29
|
-
)
|
30
|
-
assert all(
|
31
|
-
|
32
|
-
)
|
33
|
-
assert all(
|
34
|
-
|
35
|
-
)
|
36
|
-
assert all(
|
37
|
-
|
38
|
-
)
|
27
|
+
assert all(isinstance(version, tuple) for version in versions), (
|
28
|
+
"Each version should be a tuple."
|
29
|
+
)
|
30
|
+
assert all(len(version) == 2 for version in versions), (
|
31
|
+
"Each version tuple should contain two elements."
|
32
|
+
)
|
33
|
+
assert all(isinstance(version[0], str) for version in versions), (
|
34
|
+
"First element of each version tuple should be a string."
|
35
|
+
)
|
36
|
+
assert all(isinstance(version[1], str) for version in versions), (
|
37
|
+
"Second element of each version tuple should be a string."
|
38
|
+
)
|
@@ -20,11 +20,12 @@ def test_find_versions_nfound():
|
|
20
20
|
result = apkmirror.search_apk()
|
21
21
|
|
22
22
|
if result:
|
23
|
+
title = result[0]
|
23
24
|
apk_link = result[1]
|
24
25
|
|
25
26
|
assert apk_link.startswith("https://"), "APK link should be a valid URL."
|
26
27
|
|
27
|
-
download_link = apkmirror.find_version(apk_link, version)
|
28
|
+
download_link = apkmirror.find_version(apk_link, version, title)
|
28
29
|
|
29
30
|
assert download_link is None, "Version not found."
|
30
31
|
|
@@ -36,10 +37,11 @@ def test_find_versions_found():
|
|
36
37
|
result = apkmirror.search_apk()
|
37
38
|
|
38
39
|
if result:
|
40
|
+
title = result[0]
|
39
41
|
apk_link = result[1]
|
40
|
-
download_link = apkmirror.find_version(apk_link, version)
|
42
|
+
download_link = apkmirror.find_version(apk_link, version, title)
|
41
43
|
|
42
44
|
assert isinstance(download_link, str), "Download link should be a string."
|
43
|
-
assert download_link.startswith(
|
44
|
-
"
|
45
|
-
)
|
45
|
+
assert download_link.startswith("https://"), (
|
46
|
+
"Download link should be a valid URL."
|
47
|
+
)
|
@@ -24,15 +24,15 @@ def test_find_versions():
|
|
24
24
|
|
25
25
|
assert isinstance(versions, list), "Versions should be a list."
|
26
26
|
assert len(versions) > 0, "No versions found."
|
27
|
-
assert all(
|
28
|
-
|
29
|
-
)
|
30
|
-
assert all(
|
31
|
-
|
32
|
-
)
|
33
|
-
assert all(
|
34
|
-
|
35
|
-
)
|
36
|
-
assert all(
|
37
|
-
|
38
|
-
)
|
27
|
+
assert all(isinstance(version, tuple) for version in versions), (
|
28
|
+
"Each version should be a tuple."
|
29
|
+
)
|
30
|
+
assert all(len(version) == 2 for version in versions), (
|
31
|
+
"Each version tuple should contain two elements."
|
32
|
+
)
|
33
|
+
assert all(isinstance(version[0], str) for version in versions), (
|
34
|
+
"First element of each version tuple should be a string."
|
35
|
+
)
|
36
|
+
assert all(isinstance(version[1], str) for version in versions), (
|
37
|
+
"Second element of each version tuple should be a string."
|
38
|
+
)
|
@@ -24,6 +24,6 @@ def test_find_versions():
|
|
24
24
|
|
25
25
|
assert isinstance(versions, list), "Versions should be a list."
|
26
26
|
assert len(versions) > 0, "No versions found."
|
27
|
-
assert all(
|
28
|
-
|
29
|
-
)
|
27
|
+
assert all(isinstance(version, str) for version in versions), (
|
28
|
+
"Each version should be a string."
|
29
|
+
)
|
File without changes
|
@@ -1,109 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from bs4 import BeautifulSoup
|
3
|
-
import requests
|
4
|
-
|
5
|
-
|
6
|
-
class APKad:
|
7
|
-
"""
|
8
|
-
This class provides methods to search for an APK on APKAD based on package name,
|
9
|
-
and to find available versions and their download links for a given APK link.
|
10
|
-
|
11
|
-
Parameters:
|
12
|
-
pkg_name (str): The package name of the APK to search for.
|
13
|
-
|
14
|
-
Attributes:
|
15
|
-
pkg_name (str): The package name of the APK to search for.
|
16
|
-
base_url (str): The base URL of the APKAD website.
|
17
|
-
search_url (str): The URL used to search for APKs on APKAD.
|
18
|
-
headers (dict): The headers used for making HTTP requests.
|
19
|
-
session (requests.Session): The session object used for making HTTP requests.
|
20
|
-
|
21
|
-
Methods:
|
22
|
-
search_apk() -> None | tuple[str, str]:
|
23
|
-
Searches for the APK on APKAD and returns the title and link if found.
|
24
|
-
"""
|
25
|
-
|
26
|
-
def __init__(self, pkg_name: str):
|
27
|
-
self.pkg_name = pkg_name
|
28
|
-
self.base_url = "https://downloader.apk.ad"
|
29
|
-
self.api_url = "https://api.apk.ad"
|
30
|
-
self.search_url = (
|
31
|
-
self.api_url
|
32
|
-
+ f"/get?hl=en&package={self.pkg_name}&device=phone&arch=arm64-v8a&vc=&device_id="
|
33
|
-
)
|
34
|
-
self.headers = {
|
35
|
-
"accept": "text/event-stream",
|
36
|
-
"accept-language": "en-US,en;q=0.9,en-IN;q=0.8",
|
37
|
-
"cache-control": "no-cache",
|
38
|
-
"dnt": "1",
|
39
|
-
"origin": "https://downloader.apk.ad",
|
40
|
-
"pragma": "no-cache",
|
41
|
-
"priority": "u=1, i",
|
42
|
-
"referer": "https://downloader.apk.ad/",
|
43
|
-
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
44
|
-
"sec-ch-ua-mobile": "?0",
|
45
|
-
"sec-ch-ua-platform": '"Windows"',
|
46
|
-
"sec-fetch-dest": "empty",
|
47
|
-
"sec-fetch-mode": "cors",
|
48
|
-
"sec-fetch-site": "same-site",
|
49
|
-
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
50
|
-
}
|
51
|
-
self.session = requests.Session()
|
52
|
-
|
53
|
-
def search_apk(self) -> None | tuple[str, str]:
|
54
|
-
"""
|
55
|
-
Searches for the APK on APKAD and returns the title and link if found.
|
56
|
-
|
57
|
-
Returns:
|
58
|
-
None: If no matching APK is found.
|
59
|
-
tuple[str, str]: A tuple containing the title and link of the matching APK if found.
|
60
|
-
"""
|
61
|
-
url = self.search_url
|
62
|
-
response = self.session.get(url, headers=self.headers, stream=True)
|
63
|
-
stream_response = ""
|
64
|
-
for line in response.iter_lines():
|
65
|
-
if line:
|
66
|
-
line_response = line.decode("utf-8")
|
67
|
-
if '"progress":100' in line_response:
|
68
|
-
line_response = line_response[6:]
|
69
|
-
stream_response += line_response
|
70
|
-
break
|
71
|
-
if stream_response:
|
72
|
-
data = json.loads(stream_response)
|
73
|
-
html_body = data["html"]
|
74
|
-
soup = BeautifulSoup(html_body, "html.parser")
|
75
|
-
if not soup:
|
76
|
-
return None
|
77
|
-
title = soup.find("li", {"class": "_title"})
|
78
|
-
if title:
|
79
|
-
title = title.text.strip()
|
80
|
-
button = soup.find(
|
81
|
-
"button", {"onclick": True, "id": "downloadButtonapk"}
|
82
|
-
)["onclick"]
|
83
|
-
if button:
|
84
|
-
zip_args = [
|
85
|
-
arg.strip("'")
|
86
|
-
for arg in button.split("zip(")[1].split(")")[0].split(",")
|
87
|
-
]
|
88
|
-
h = zip_args[0] # hash
|
89
|
-
p = zip_args[-1] # type
|
90
|
-
token = zip_args[1] # token
|
91
|
-
ip = zip_args[2] # ip
|
92
|
-
google_id = zip_args[3] # package_name
|
93
|
-
t = zip_args[4] # time
|
94
|
-
apk_url = f"https://zip.apk.ad/compress?h={h}&p={p}&token={token}&ip={ip}&google_id={google_id}&t={t}"
|
95
|
-
apk_url_response = self.session.get(
|
96
|
-
url=apk_url, headers=self.headers, stream=True
|
97
|
-
)
|
98
|
-
for line in apk_url_response.iter_lines():
|
99
|
-
if line:
|
100
|
-
line_response = line.decode("utf-8")
|
101
|
-
if "File is ready for download." in line_response:
|
102
|
-
line_response = json.loads(line_response)
|
103
|
-
line_html = line_response["html"]
|
104
|
-
line_soup = BeautifulSoup(line_html, "html.parser")
|
105
|
-
download_link = line_soup.find("a")["href"]
|
106
|
-
if download_link.endswith("\n"):
|
107
|
-
download_link = download_link[:-1]
|
108
|
-
return title, download_link
|
109
|
-
return None
|
File without changes
|
File without changes
|
@@ -1,9 +1,9 @@
|
|
1
|
-
from .sites.
|
2
|
-
from .sites.apkmirror import APKMirror
|
3
|
-
from .sites.appteka import AppTeka
|
1
|
+
from .sites.apkad import APKad
|
4
2
|
from .sites.apkcombo import APKCombo
|
5
3
|
from .sites.apkfab import APKFab
|
6
|
-
from .sites.
|
4
|
+
from .sites.apkmirror import APKMirror
|
5
|
+
from .sites.apkpure import APKPure
|
6
|
+
from .sites.appteka import AppTeka
|
7
7
|
from .sites.aptoide import Aptoide
|
8
8
|
|
9
9
|
__all__ = [
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|