diff --git a/api/main.py b/api/main.py
index 7d0db38..6b55d61 100644
--- a/api/main.py
+++ b/api/main.py
@@ -3,7 +3,7 @@ import json
import logging
from contextlib import asynccontextmanager
from io import BytesIO
-from typing import Literal
+from typing import Literal, Annotated
import aiohttp
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@@ -18,6 +18,7 @@ from fastapi import (
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
+from starlette.responses import HTMLResponse
from api import middleware
from api.scheduler import setup_scheduler
@@ -193,6 +194,7 @@ async def configure(
"authentication_required": settings.api_password is not None
and not settings.is_public_instance,
"kodi_code": kodi_code,
+ "disable_download_via_browser": settings.disable_download_via_browser,
},
)
@@ -643,6 +645,79 @@ async def get_poster(
raise HTTPException(status_code=404, detail="Failed to create poster.")
+@app.get(
+ "/download/{secret_str}/{catalog_type}/{video_id}",
+ response_class=HTMLResponse,
+ tags=["download"],
+)
+@app.get(
+ "/download/{secret_str}/{catalog_type}/{video_id}/{season}/{episode}",
+ response_class=HTMLResponse,
+ tags=["download"],
+)
+@wrappers.auth_required
+async def download_info(
+ request: Request,
+ secret_str: str,
+ catalog_type: Literal["movie", "series"],
+ video_id: str,
+ user_data: Annotated[schemas.UserData, Depends(get_user_data)],
+ background_tasks: BackgroundTasks,
+ season: int = None,
+ episode: int = None,
+):
+ if (
+ not user_data.streaming_provider
+ or not user_data.streaming_provider.download_via_browser
+ or settings.disable_download_via_browser
+ ):
+ raise HTTPException(
+ status_code=403,
+ detail="Download option is not enabled or no streaming provider configured",
+ )
+
+ metadata = (
+ await crud.get_movie_data_by_id(video_id)
+ if catalog_type == "movie"
+ else await crud.get_series_data_by_id(video_id)
+ )
+ if not metadata:
+ raise HTTPException(status_code=404, detail="Metadata not found")
+
+ user_ip = await get_user_public_ip(request, user_data)
+
+ if catalog_type == "movie":
+ streams = await crud.get_movie_streams(
+ user_data, secret_str, video_id, user_ip, background_tasks
+ )
+ else:
+ streams = await crud.get_series_streams(
+ user_data, secret_str, video_id, season, episode, user_ip, background_tasks
+ )
+
+ streaming_provider_path = f"{settings.host_url}/streaming_provider/"
+ downloadable_streams = [
+ stream
+ for stream in streams
+ if stream.url and stream.url.startswith(streaming_provider_path)
+ ]
+
+ context = {
+ "title": metadata.title,
+ "year": metadata.year,
+ "poster": metadata.poster,
+ "description": metadata.description,
+ "streams": downloadable_streams,
+ "catalog_type": catalog_type,
+ "season": season,
+ "episode": episode,
+ }
+
+ return TEMPLATES.TemplateResponse(
+ "html/download_info.html", {"request": request, **context}
+ )
+
+
app.include_router(
streaming_provider_router, prefix="/streaming_provider", tags=["streaming_provider"]
)
diff --git a/db/config.py b/db/config.py
index 83029bf..90d463b 100644
--- a/db/config.py
+++ b/db/config.py
@@ -45,6 +45,7 @@ class Settings(BaseSettings):
prowlarr_live_title_search: bool = False
prowlarr_background_title_search: bool = True
prowlarr_search_query_timeout: int = 120
+ disable_download_via_browser: bool = False
# Scheduler settings
disable_all_scheduler: bool = False
diff --git a/db/schemas.py b/db/schemas.py
index 9d95235..7392b44 100644
--- a/db/schemas.py
+++ b/db/schemas.py
@@ -142,6 +142,7 @@ class StreamingProvider(BaseModel):
password: str | None = Field(default=None, alias="pw")
enable_watchlist_catalogs: bool = Field(default=True, alias="ewc")
qbittorrent_config: QBittorrentConfig | None = Field(default=None, alias="qbc")
+ download_via_browser: bool = Field(default=False, alias="dvb")
@model_validator(mode="after")
def validate_token_or_username_password(self) -> "StreamingProvider":
@@ -172,6 +173,7 @@ class UserData(BaseModel):
default=const.RESOLUTIONS, alias="sr"
)
enable_catalogs: bool = Field(default=True, alias="ec")
+ enable_imdb_metadata: bool = Field(default=True, alias="eim")
max_size: int | str | float = Field(default=math.inf, alias="ms")
max_streams_per_resolution: int = Field(default=3, alias="mspr")
show_full_torrent_name: bool = Field(default=True, alias="sftn")
diff --git a/mediafusion_scrapy/task.py b/mediafusion_scrapy/task.py
index a433783..dcb9e8d 100644
--- a/mediafusion_scrapy/task.py
+++ b/mediafusion_scrapy/task.py
@@ -25,4 +25,4 @@ def run_spider(spider_name: str, *args, **kwargs):
if __name__ == "__main__":
- run_spider_in_process("streamed")
+ run_spider_in_process("sport_video")
diff --git a/resources/html/configure.html b/resources/html/configure.html
index 09fb64a..9679fd1 100644
--- a/resources/html/configure.html
+++ b/resources/html/configure.html
@@ -207,16 +207,26 @@
-
-
-
Provider Watchlist Catalog:
-
+
+
+
+
Enable IMDb Title Meta Data Response:
+
+
+
diff --git a/resources/html/download_info.html b/resources/html/download_info.html
new file mode 100644
index 0000000..867d4b8
--- /dev/null
+++ b/resources/html/download_info.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+ Download Options - {{ title }}
+
+
+
+
+
+
+
+
+
+

+
Download Options - {{ title }} ({{ year }})
+
+
+
+

+
+
+ {% if description %}
{{ description }}
{% endif %}
+ {% if catalog_type == "series" %}
+
Season: {{ season }}, Episode: {{ episode }}
+ {% endif %}
+
+
🎬 Available Streams
+ {% if streams %}
+
+ {% for stream in streams %}
+
+
{{ stream.name }}
+
+ {% for line in stream.description.split('\n') %}
+
{{ line | safe }}
+ {% endfor %}
+
+
+
+ {% endfor %}
+
+ {% else %}
+
No download options available.
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/config_script.js b/resources/js/config_script.js
index 22a15fc..45a2eaa 100644
--- a/resources/js/config_script.js
+++ b/resources/js/config_script.js
@@ -194,12 +194,12 @@ function updateProviderFields(isChangeEvent = false) {
setElementDisplay('token_input', 'block');
setElementDisplay('qbittorrent_config', 'none');
}
- setElementDisplay('watchlist_section', 'block');
+ setElementDisplay('streaming_provider_options', 'block');
watchlistLabel.textContent = `Enable ${provider.charAt(0).toUpperCase() + provider.slice(1)} Watchlist`;
} else {
setElementDisplay('credentials', 'none');
setElementDisplay('token_input', 'none');
- setElementDisplay('watchlist_section', 'none');
+ setElementDisplay('streaming_provider_options', 'none');
setElementDisplay('qbittorrent_config', 'none');
}
@@ -300,6 +300,9 @@ function getUserData() {
}
streamingProviderData.service = provider;
streamingProviderData.enable_watchlist_catalogs = document.getElementById('enable_watchlist').checked;
+ if (document.getElementById('download_via_browser')) {
+ streamingProviderData.download_via_browser = document.getElementById('download_via_browser').checked;
+ }
} else {
streamingProviderData = null;
}
@@ -365,6 +368,7 @@ function getUserData() {
selected_catalogs: Array.from(document.querySelectorAll('input[name="selected_catalogs"]:checked')).map(el => el.value),
selected_resolutions: Array.from(document.querySelectorAll('input[name="selected_resolutions"]:checked')).map(el => el.value || null),
enable_catalogs: document.getElementById('enable_catalogs').checked,
+ enable_imdb_metadata: document.getElementById('enable_imdb_metadata').checked,
max_size: maxSizeBytes,
max_streams_per_resolution: maxStreamsPerResolution,
torrent_sorting_priority: selectedSortingOptions,
diff --git a/streaming_providers/routes.py b/streaming_providers/routes.py
index 0f8174f..bf6d0b0 100644
--- a/streaming_providers/routes.py
+++ b/streaming_providers/routes.py
@@ -1,5 +1,6 @@
import asyncio
import logging
+from os import path
from fastapi import (
Request,
@@ -54,6 +55,11 @@ async def get_cached_stream_url_and_redirect(
"/proxy/stream",
cached_stream_url,
query_params={"api_password": user_data.mediaflow_config.api_password},
+ response_headers={
+ "Content-Disposition": "attachment, filename={}".format(
+ path.basename(cached_stream_url)
+ )
+ },
)
return RedirectResponse(
url=cached_stream_url, headers=response.headers, status_code=302
@@ -131,11 +137,16 @@ def apply_mediaflow_proxy_if_needed(video_url, user_data):
"/proxy/stream",
video_url,
query_params={"api_password": user_data.mediaflow_config.api_password},
+ response_headers={
+ "Content-Disposition": "attachment, filename={}".format(
+ path.basename(video_url)
+ )
+ },
)
return video_url
-def handle_provider_exception(error, usage):
+def handle_provider_exception(error, usage) -> str:
"""
Handles exceptions raised by the provider and logs them.
"""
@@ -148,7 +159,7 @@ def handle_provider_exception(error, usage):
return f"{settings.host_url}/static/exceptions/{error.video_file_name}"
-def handle_generic_exception(exception, info_hash):
+def handle_generic_exception(exception, info_hash) -> str:
"""
Handles generic exceptions and logs them.
"""
@@ -272,11 +283,11 @@ async def delete_all_watchlist(
except ProviderException as error:
# Handle provider-specific exceptions
- video_url, _ = handle_provider_exception(error, "delete_watchlist")
+ video_url = handle_provider_exception(error, "delete_watchlist")
except Exception as e:
# Handle generic exceptions
- video_url, _ = handle_generic_exception(e, "delete_watchlist")
+ video_url = handle_generic_exception(e, "delete_watchlist")
return RedirectResponse(url=video_url, headers=response.headers)
diff --git a/utils/network.py b/utils/network.py
index ee2841d..643bd2c 100644
--- a/utils/network.py
+++ b/utils/network.py
@@ -252,6 +252,7 @@ def encode_mediaflow_proxy_url(
destination_url: str | None = None,
query_params: dict | None = None,
request_headers: dict | None = None,
+ response_headers: dict | None = None,
) -> str:
query_params = query_params or {}
if destination_url is not None:
@@ -262,6 +263,10 @@ def encode_mediaflow_proxy_url(
query_params.update(
{f"h_{key}": value for key, value in request_headers.items()}
)
+ if response_headers:
+ query_params.update(
+ {f"r_{key}": value for key, value in response_headers.items()}
+ )
# Encode the query parameters
encoded_params = parse.urlencode(query_params, quote_via=parse.quote)
diff --git a/utils/parser.py b/utils/parser.py
index 6c58b06..58097e9 100644
--- a/utils/parser.py
+++ b/utils/parser.py
@@ -159,6 +159,11 @@ async def parse_stream_data(
else "P2P"
)
has_streaming_provider = user_data.streaming_provider is not None
+ download_via_browser = (
+ has_streaming_provider
+ and user_data.streaming_provider.download_via_browser
+ and not settings.disable_download_via_browser
+ )
base_proxy_url_template = ""
if has_streaming_provider:
@@ -263,6 +268,18 @@ async def parse_stream_data(
stream_list.append(Stream(**stream_details))
+ if stream_list and download_via_browser:
+ download_url = f"{settings.host_url}/download/{secret_str}/{'series' if is_series else 'movie'}/{streams[0].meta_id}"
+ if is_series:
+ download_url += f"/{season}/{episode}"
+ stream_list.append(
+ Stream(
+ name=f"{settings.addon_name} {streaming_provider_name} 📥",
+ description="📥 Download Torrent Streams via WebBrowser",
+ externalUrl=download_url,
+ )
+ )
+
return stream_list
@@ -405,6 +422,9 @@ async def generate_manifest(manifest: dict, user_data: UserData) -> dict:
break
manifest["catalogs"] = ordered_catalogs
+ if not user_data.enable_imdb_metadata:
+ # Remove IMDb metadata if disabled
+ resources[-1]["idPrefixes"].remove("tt")
else:
# If catalogs are not enabled, clear them from the manifest
manifest["catalogs"] = []