Skip to content

Commit 26e23d6

Browse files
committed
added shareus, bitly, tinyurl support release v0.0.5 ✨
1 parent 73a2923 commit 26e23d6

File tree

4 files changed

+279
-28
lines changed

4 files changed

+279
-28
lines changed

README.md

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ To Upgrade
1414
pip install --upgrade link-shortly
1515
```
1616

17-
## Quick Usage Example ( supports sync )
17+
## Quick Usage Example
1818
```python
1919
from shortly import Shortly
2020

@@ -28,24 +28,6 @@ if __name__ == "__main__":
2828
main()
2929
```
3030

31-
## Quick Usage Example ( supports async )
32-
```python
33-
import asyncio
34-
from shortly import Shortly
35-
36-
# Initialize Shortly
37-
shortly = Shortly(api_key='<YOUR API KEY>', base_url='<YOUR BASE SITE>')
38-
39-
async def main():
40-
# Async call to convert URL
41-
link = await shortly.convert("https://example.com/long-url")
42-
print(link)
43-
44-
if __name__ == "__main__":
45-
asyncio.run(main())
46-
```
47-
48-
4931
## Error Handling
5032

5133
The library comes with built-in exception handling to manage common errors such as invalid links, not found links, timeouts, or connection issues.

src/shortly/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"""
1010

1111

12-
__version__ = "0.0.4"
12+
__version__ = "0.0.5"
1313
__author__ = "RknDeveloper"
1414
__license__ = "MIT License "
1515
__copyright__ = "Copyright (C) 2025-present RknDeveloper <https://github.com/RknDeveloper>"

src/shortly/shortly.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010

1111
import asyncio
1212
import functools
13-
from .utils import convert as _utils_convert
13+
from .utils import (
14+
adlinkfy_convert,
15+
bitly_convert,
16+
tinyurl_convert,
17+
shareus_convert
18+
)
19+
1420
from .exceptions import (
1521
ShortlyValueError
1622
)
@@ -34,12 +40,18 @@ def __init__(self, api_key=None, base_url=None):
3440
"""
3541
if not base_url or not isinstance(base_url, str):
3642
raise ShortlyValueError("base_url must be a non-empty string")
37-
if not api_key or not isinstance(api_key, str):
38-
raise ShortlyValueError("api_key must be a non-empty string")
39-
40-
self.api_key = api_key
43+
4144
self.base_url = (urlparse(base_url).netloc or urlparse(base_url).path).rstrip("/")
4245

46+
if self.base_url == "tinyurl.com":
47+
# api_key optional
48+
self.api_key = api_key
49+
else:
50+
# api_key required
51+
if not api_key or not isinstance(api_key, str):
52+
raise ShortlyValueError(f"api_key must be a non-empty string for {self.base_url}")
53+
self.api_key = api_key
54+
4355
# Internal async method calling utils.convert
4456
async def _convert_async(self, link, alias=None, silently=False, timeout=10):
4557
"""
@@ -54,7 +66,16 @@ async def _convert_async(self, link, alias=None, silently=False, timeout=10):
5466
Output:
5567
Returns shortened link or error response from utils.convert
5668
"""
57-
return await _utils_convert(self, link, alias, silently, timeout)
69+
if self.base_url == "tinyurl.com":
70+
self.shortner = await tinyurl_convert(self, link, alias, silently, timeout)
71+
elif self.base_url == "shareus.io":
72+
self.shortner = await shareus_convert(self, link, alias, silently, timeout)
73+
elif self.base_url == "bitly.com":
74+
self.shortner = await bitly_convert(self, link, alias, silently, timeout)
75+
else:
76+
self.shortner = await adlinkfy_convert(self, link, alias, silently, timeout)
77+
78+
return self.shortner
5879

5980

6081
# -------------------------------

src/shortly/utils.py

Lines changed: 250 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
)
2121

2222

23-
async def convert(self, link, alias=None, silently=False, timeout=10):
23+
async def adlinkfy_convert(self, link, alias=None, silently=False, timeout=10):
2424
"""
2525
Shorten a URL using Link Shortly/All Adlinkfy API.
2626
@@ -86,4 +86,252 @@ async def convert(self, link, alias=None, silently=False, timeout=10):
8686
except aiohttp.ClientConnectionError:
8787
raise ShortlyConnectionError(f"Failed to connect to {self.base_url}.")
8888
except Exception as e:
89-
raise ShortlyError(f"An unexpected error occurred: {e}")
89+
raise ShortlyError(f"An unexpected error occurred: {e}")
90+
91+
async def shareus_convert(self, link, alias=None, silently=False, timeout=10):
92+
"""
93+
Shorten a URL using Shareus.io API.
94+
95+
Parameters:
96+
api_key (str): Your API key for the Shareus service.
97+
base_url (str): The domain of the API (should be "shareus.io").
98+
link (str): The long URL you want to shorten.
99+
alias (str, optional): Custom alias for the short link. Default is None.
100+
silently (bool): If True, the function will directly return the original URL without raising errors.
101+
timeout (int, optional): Maximum seconds to wait for API response. Default is 10.
102+
103+
Returns:
104+
str: The shortened URL returned by the API.
105+
106+
Raises:
107+
ShortlyInvalidLinkError: If the provided link is invalid or malformed.
108+
ShortlyLinkNotFoundError: If the short link does not exist or has expired.
109+
ShortlyTimeoutError: If request exceeds the allowed timeout.
110+
ShortlyConnectionError: If cannot connect to API server.
111+
ShortlyError: For other API-related errors.
112+
"""
113+
if silently:
114+
return link
115+
116+
# Shareus.io API configuration
117+
api_url = f"https://api.{self.base_url}/easy_api"
118+
params = {"key": self.api_key, "link": link}
119+
120+
if alias:
121+
params["alias"] = alias
122+
123+
try:
124+
async with aiohttp.ClientSession() as session:
125+
headers = {
126+
"User-Agent": (
127+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
128+
"AppleWebKit/537.36 (KHTML, like Gecko) "
129+
"Chrome/91.0.4472.124 Safari/537.36"
130+
)
131+
}
132+
133+
async with session.get(api_url, params=params, headers=headers, timeout=timeout) as response:
134+
if response.status != 200:
135+
raise ShortlyError(f"Failed to shorten your link (HTTP {response.status}).")
136+
137+
# Shareus returns plain text response
138+
data_text = await response.text()
139+
140+
if data_text == "settings not saved":
141+
raise ShortlyError("Settings not saved or invalid API key.")
142+
143+
# Check if response looks like an error
144+
if any(error_indicator in data_text.lower() for error_indicator in ["error", "invalid", "not found", "failed"]):
145+
if "invalid" in data_text.lower():
146+
raise ShortlyInvalidLinkError(data_text)
147+
elif "not found" in data_text.lower() or "expired" in data_text.lower():
148+
raise ShortlyLinkNotFoundError(data_text)
149+
else:
150+
raise ShortlyError(data_text)
151+
152+
# If we get here, it's likely a successful shortening
153+
return data_text
154+
155+
except asyncio.TimeoutError:
156+
raise ShortlyTimeoutError(f"Request timed out after {timeout} seconds.")
157+
except aiohttp.ClientConnectionError:
158+
raise ShortlyConnectionError(f"Failed to connect to {self.base_url}.")
159+
except json.JSONDecodeError:
160+
# This is expected for Shareus as it returns plain text, not JSON
161+
raise ShortlyJsonDecodeError(f"Json Error: {e}")
162+
except Exception as e:
163+
raise ShortlyError(f"An unexpected error occurred: {e}")
164+
165+
async def tinyurl_convert(self, link, alias=None, silently=False, timeout=10):
166+
"""
167+
Shorten a URL using TinyURL API - supports both old (no token) and new (with token) APIs
168+
"""
169+
if silently:
170+
return link
171+
172+
# Check if we have a token to decide which API to use
173+
if hasattr(self, 'api_key') and self.api_key:
174+
return await _tinyurl_new_api(self, link, alias, timeout)
175+
else:
176+
return await _tinyurl_old_api(self, link, alias, timeout)
177+
178+
async def _tinyurl_old_api(self, link, alias=None, timeout=10):
179+
"""
180+
Old TinyURL API (no token required)
181+
Docs: http://tinyurl.com/api-create.php
182+
"""
183+
api_url = f"http://{self.base_url}/api-create.php"
184+
params = {"url": link}
185+
186+
if alias:
187+
params["alias"] = alias
188+
189+
try:
190+
async with aiohttp.ClientSession() as session:
191+
async with session.get(api_url, params=params, timeout=timeout) as response:
192+
if response.status != 200:
193+
raise ShortlyError(f"Failed to shorten link (TinyURL error: {response.status}).")
194+
195+
shortened_url = await response.text()
196+
197+
if "Error:" in shortened_url:
198+
raise ShortlyError(f"TinyURL error: {shortened_url}")
199+
200+
return shortened_url.strip()
201+
202+
except asyncio.TimeoutError:
203+
raise ShortlyTimeoutError(f"TinyURL request timed out after {timeout} seconds.")
204+
except aiohttp.ClientConnectionError:
205+
raise ShortlyConnectionError("Failed to connect to TinyURL.")
206+
except Exception as e:
207+
raise ShortlyError(f"TinyURL unexpected error: {e}")
208+
209+
async def _tinyurl_new_api(self, link, alias=None, timeout=10):
210+
"""
211+
New TinyURL API (requires token)
212+
Docs: https://tinyurl.com/app/dev/api
213+
"""
214+
215+
216+
api_url = f"https://api.{self.base_url}/create"
217+
headers = {
218+
"Authorization": f"Bearer {self.api_key}",
219+
"Content-Type": "application/json"
220+
}
221+
222+
payload = {
223+
"url": link,
224+
"domain": "tinyurl.com"
225+
}
226+
227+
if alias:
228+
payload["alias"] = alias
229+
230+
try:
231+
async with aiohttp.ClientSession() as session:
232+
async with session.post(
233+
api_url,
234+
json=payload,
235+
headers=headers,
236+
timeout=timeout
237+
) as response:
238+
239+
# 🔹 Safe JSON parsing with better error handling
240+
try:
241+
response_data = await response.json(content_type=None)
242+
except Exception:
243+
response_text = await response.text()
244+
response_data = {"raw": response_text}
245+
246+
# Handle case where alias already exists (returns string)
247+
if "alias already exists" in response_text.lower():
248+
raise ShortlyError("Custom alias already exists. Please choose a different one.")
249+
250+
if response.status == 200 and "data" in response_data:
251+
return response_data["data"]["tiny_url"]
252+
253+
elif response.status == 401:
254+
raise ShortlyError("Invalid TinyURL API token.")
255+
256+
elif response.status == 422:
257+
error_msg = "Unknown validation error"
258+
if isinstance(response_data, dict):
259+
errors = response_data.get("errors")
260+
if isinstance(errors, list) and errors:
261+
error_msg = errors[0].get("message", error_msg)
262+
elif isinstance(response_data, str):
263+
error_msg = response_data
264+
raise ShortlyError(f"TinyURL validation error: {error_msg}")
265+
266+
elif response.status == 429:
267+
raise ShortlyError("TinyURL rate limit exceeded. Please try again later.")
268+
269+
else:
270+
# Handle string responses gracefully
271+
if isinstance(response_data, str):
272+
error_message = response_data
273+
elif isinstance(response_data, dict) and "raw" in response_data:
274+
error_message = response_data["raw"]
275+
else:
276+
error_message = str(response_data)
277+
278+
raise ShortlyError(f"TinyURL API error: {response.status} - {error_message}")
279+
280+
except asyncio.TimeoutError:
281+
raise ShortlyTimeoutError(f"TinyURL request timed out after {timeout} seconds.")
282+
except aiohttp.ClientConnectionError:
283+
raise ShortlyConnectionError("Failed to connect to TinyURL API.")
284+
except Exception as e:
285+
raise ShortlyError(f"TinyURL unexpected error: {e}")
286+
287+
async def bitly_convert(self, link, alias=None, silently=False, timeout=10):
288+
"""
289+
Bitly API का उपयोग करके URL को छोटा (shorten) करें।
290+
Docs: https://dev.bitly.com/api-reference
291+
"""
292+
if silently:
293+
return link
294+
295+
api_url = f"https://api-ssl.{self.base_url}/v4/shorten"
296+
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
297+
payload = {"long_url": link}
298+
299+
# Correct way to add custom alias
300+
if alias:
301+
payload["custom_bitlink"] = alias
302+
303+
try:
304+
async with aiohttp.ClientSession() as session:
305+
async with session.post(api_url, headers=headers, json=payload, timeout=timeout) as response:
306+
307+
# Different error cases handle करें
308+
if response.status == 401:
309+
raise ShortlyError("Invalid Bitly API key")
310+
elif response.status == 400:
311+
error_text = await response.text()
312+
if "CUSTOM_BITLINK_ALREADY_EXISTS" in error_text:
313+
raise ShortlyError(f"Custom alias '{alias}' already exists on Bitly")
314+
elif "INVALID_CUSTOM_BITLINK" in error_text:
315+
raise ShortlyError(f"Invalid custom alias: {alias}")
316+
else:
317+
raise ShortlyError("Invalid URL provided to Bitly")
318+
elif response.status == 403:
319+
raise ShortlyError("Bitly API permission denied")
320+
elif response.status == 409:
321+
raise ShortlyError("Custom alias already exists")
322+
elif response.status != 200 and response.status != 201:
323+
raise ShortlyError(f"Bitly error: {await response.text()}")
324+
325+
try:
326+
data = await response.json()
327+
except Exception as e:
328+
raise ShortlyJsonDecodeError(f"Invalid JSON from Bitly: {e}")
329+
330+
return data.get("link")
331+
332+
except asyncio.TimeoutError:
333+
raise ShortlyTimeoutError(f"Bitly request timed out after {timeout} seconds.")
334+
except aiohttp.ClientConnectionError:
335+
raise ShortlyConnectionError("Failed to connect to Bitly.")
336+
except Exception as e:
337+
raise ShortlyError(f"Bitly unexpected error: {e}")

0 commit comments

Comments
 (0)