Skip to content

Commit

Permalink
Add StremThru as streaming provider (#345)
Browse files Browse the repository at this point in the history
* Add StremThru as streaming provider

* Refactoring on Stremthru

---------

Co-authored-by: mhdzumair <[email protected]>
  • Loading branch information
MunifTanjim and mhdzumair authored Nov 19, 2024
1 parent 9b88c7f commit 73c9bf2
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 1 deletion.
2 changes: 2 additions & 0 deletions db/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ class StreamingProvider(BaseModel):
"torbox",
"premiumize",
"qbittorrent",
"stremthru",
] = Field(alias="sv")
url: HttpUrl | None = Field(default=None, alias="u")
token: str | None = Field(default=None, alias="tk")
email: str | None = Field(default=None, alias="em")
password: str | None = Field(default=None, alias="pw")
Expand Down
2 changes: 1 addition & 1 deletion resources/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
font-size: 1rem;
}

#signup_section, #oauth_section, #token_input {
#service_url_section, #signup_section, #oauth_section, #token_input {
margin-top: 1rem;
}

Expand Down
11 changes: 11 additions & 0 deletions resources/html/configure.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,19 @@ <h4 class="section-header">Streaming Provider Configuration</h4>
<option value="premiumize" {% if user_data.streaming_provider.service=='premiumize' %}selected{% endif %}>Premiumize (Premium)</option>
<option value="alldebrid" {% if user_data.streaming_provider.service=='alldebrid' %}selected{% endif %}>AllDebrid (Premium)</option>
<option value="qbittorrent" {% if user_data.streaming_provider.service=='qbittorrent' %}selected{% endif %}>qBittorrent - WebDav (Free/Premium)</option>
<option value="stremthru" {% if user_data.streaming_provider.service=='stremthru' %}selected{% endif %}>StremThru (Interface)</option>
</select>

<!-- Service URL -->
<div id="service_url_section" style="display:none" class="mb-4">
<label for="service_url">Service URL:</label>
<input class="form-control" type="text" id="service_url" name="service_url" placeholder="https://" aria-label="Service URL"
value="{{ user_data.streaming_provider.url if user_data.streaming_provider.url else '' }}">
<div class="invalid-feedback">
Service URL should be valid.
</div>
</div>

<!-- Affiliate Signup Links -->
<div id="signup_section" style="display:none" class="mb-4">
<h6>Don't have an account?</h6>
Expand Down
30 changes: 30 additions & 0 deletions resources/js/config_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const oAuthBtn = document.getElementById('oauth_btn');
let currentAuthorizationToken = null;
const servicesRequiringCredentials = ['pikpak',];
const servicesRequiringUrl = ['stremthru'];
const providerSignupLinks = {
pikpak: 'https://mypikpak.com/drive/activity/invited?invitation-code=52875535',
seedr: 'https://www.seedr.cc/?r=2726511',
Expand Down Expand Up @@ -168,9 +169,15 @@ function adjustOAuthSectionDisplay() {

function updateProviderFields(isChangeEvent = false) {
const provider = document.getElementById('provider_service').value;
const serviceUrlInput = document.getElementById('service_url');
const tokenInput = document.getElementById('provider_token');
const watchlistLabel = document.getElementById('watchlist_label');

if (servicesRequiringUrl.includes(provider)) {
setElementDisplay('service_url_section', 'block');
} else {
setElementDisplay('service_url_section', 'none');
}

if (provider in providerSignupLinks) {
document.getElementById('signup_link').href = providerSignupLinks[provider];
Expand Down Expand Up @@ -205,6 +212,7 @@ function updateProviderFields(isChangeEvent = false) {

// Reset the fields only if this is triggered by an onchange event
if (isChangeEvent) {
serviceUrlInput.value = '';
tokenInput.value = '';
tokenInput.disabled = false;
currentAuthorizationToken = null;
Expand Down Expand Up @@ -313,6 +321,28 @@ function getUserData() {

// Validate and collect streaming provider data
if (provider) {
if (servicesRequiringUrl.includes(provider)) {
const serviceUrl = document.getElementById('service_url').value.trim();
if (!serviceUrl) {
validateInput('service_url', false);
streamingProviderData.url = '';
return;
}
let isValidUrl = false;
try {
const url = new URL(serviceUrl);
const hostname = url.hostname.toLowerCase();
// Prevent localhost and internal IPs
isValidUrl = url.protocol === 'https:' &&
!hostname.includes('localhost') &&
!hostname.match(/^127\.|^192\.168\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\./);
} catch (_) {
isValidUrl = false;
}
validateInput('service_url', isValidUrl);
// Basic URL sanitization
streamingProviderData.url = isValidUrl ? serviceUrl : '';
}
if (servicesRequiringCredentials.includes(provider)) {
validateInput('email', document.getElementById('email').value);
validateInput('password', document.getElementById('password').value);
Expand Down
12 changes: 12 additions & 0 deletions streaming_providers/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
get_video_url_from_torbox,
validate_torbox_credentials,
)
from streaming_providers.stremthru.utils import (
update_st_cache_status,
fetch_downloaded_info_hashes_from_st,
delete_all_torrents_from_st,
get_video_url_from_stremthru,
validate_stremthru_credentials,
)

# Define provider-specific cache update functions
CACHE_UPDATE_FUNCTIONS = {
Expand All @@ -73,6 +80,7 @@
"torbox": update_torbox_cache_status,
"premiumize": update_pm_cache_status,
"qbittorrent": update_qbittorrent_cache_status,
"stremthru": update_st_cache_status,
}

# Define provider-specific downloaded info hashes fetch functions
Expand All @@ -86,6 +94,7 @@
"torbox": fetch_downloaded_info_hashes_from_torbox,
"premiumize": fetch_downloaded_info_hashes_from_premiumize,
"qbittorrent": fetch_info_hashes_from_webdav,
"stremthru": fetch_downloaded_info_hashes_from_st,
}


Expand All @@ -99,6 +108,7 @@
"seedr": delete_all_torrents_from_seedr,
"offcloud": delete_all_torrents_from_oc,
"torbox": delete_all_torrents_from_torbox,
"stremthru": delete_all_torrents_from_st,
}


Expand All @@ -112,6 +122,7 @@
"realdebrid": get_video_url_from_realdebrid,
"seedr": get_video_url_from_seedr,
"torbox": get_video_url_from_torbox,
"stremthru": get_video_url_from_stremthru,
}


Expand All @@ -125,4 +136,5 @@
"realdebrid": validate_realdebrid_credentials,
"seedr": validate_seedr_credentials,
"torbox": validate_torbox_credentials,
"stremthru": validate_stremthru_credentials,
}
Empty file.
125 changes: 125 additions & 0 deletions streaming_providers/stremthru/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from typing import Any, Optional

from streaming_providers.debrid_client import DebridClient
from streaming_providers.exceptions import ProviderException


class StremThruError(Exception):
def __init__(self, error: dict[str, Any]):
self.type = error.get("type", "")
self.code = error.get("code", "")
self.message = error.get("message", "")
self.store_name = error.get("store_name", "")


class StremThru(DebridClient):
AGENT = "mediafusion"

def __init__(self, url: str, token: str, **kwargs):
self.BASE_URL = url
super().__init__(token)

async def initialize_headers(self):
self.headers = {
"Proxy-Authorization": f"Basic {self.token}",
"User-Agent": self.AGENT,
}

def __del__(self):
pass

async def _handle_service_specific_errors(self, error_data: dict, status_code: int):
pass

async def disable_access_token(self):
pass

async def _make_request(
self,
method: str,
url: str,
data: Optional[dict] = None,
json: Optional[dict] = None,
params: Optional[dict] = None,
is_return_none: bool = False,
is_expected_to_fail: bool = False,
retry_count: int = 0,
) -> dict[str, Any]:
params = params or {}
url = self.BASE_URL + url
response = await super()._make_request(
method,
url,
data,
json,
params,
is_return_none,
is_expected_to_fail,
retry_count,
)
if response.get("error"):
if is_expected_to_fail:
return response
error_message = response.get("error", "unknown error")
raise ProviderException(error_message, "api_error.mp4")
return response.get("data")

@staticmethod
def _validate_error_response(response_data: dict):
if response_data.get("error", None):
raise ProviderException(
f"Failed request to StremThru {response_data['error']}",
"transfer_error.mp4",
)

async def add_magnet_link(self, magnet_link):
response_data = await self._make_request(
"POST", "/v0/store/magnets", data={"magnet": magnet_link}
)
self._validate_error_response(response_data)
return response_data["data"]

async def get_user_torrent_list(self):
return await self._make_request("GET", "/v0/store/magnets")

async def get_torrent_info(self, torrent_id):
response = await self._make_request("GET", "/v0/store/magnets/" + torrent_id)
return response.get("data", {})

async def get_torrent_instant_availability(self, magnet_links: list[str]):
return await self._make_request(
"GET", "/v0/store/magnets/check", params={"magnet": ",".join(magnet_links)}
)

async def get_available_torrent(self, info_hash) -> dict[str, Any] | None:
available_torrents = await self.get_user_torrent_list()
self._validate_error_response(available_torrents)
if not available_torrents.get("data"):
return None
for torrent in available_torrents["data"]["items"]:
if torrent["hash"] == info_hash:
return torrent

async def create_download_link(self, link):
response = await self._make_request(
"POST",
"/v0/store/link/generate",
data={"link": link},
is_expected_to_fail=True,
)
if response.get("data"):
return response["data"]
error_message = response.get("error", "unknown error")
raise ProviderException(
f"Failed to create download link from StremThru {error_message}",
"transfer_error.mp4",
)

async def delete_torrent(self, magnet_id):
return await self._make_request(
"DELETE",
"/v0/store/magnets/" + magnet_id,
)

async def get_user_info(self):
return await self._make_request("GET", "/v0/store/user")
Loading

0 comments on commit 73c9bf2

Please sign in to comment.