mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
#102: Add option to dynamic sorting, max streams per resolution
#113: Add option to display full torrent name
This commit is contained in:
+10
-1
@@ -138,13 +138,22 @@ async def configure(
|
||||
if user_data.streaming_provider:
|
||||
user_data.streaming_provider.password = None
|
||||
|
||||
# Prepare catalogs based on user preferences or default order
|
||||
sorted_catalogs = sorted(
|
||||
zip(const.CATALOG_ID_DATA, const.CATALOG_NAME_DATA),
|
||||
key=lambda x: user_data.selected_catalogs.index(x[0])
|
||||
if x[0] in user_data.selected_catalogs
|
||||
else len(user_data.selected_catalogs),
|
||||
)
|
||||
|
||||
return TEMPLATES.TemplateResponse(
|
||||
"html/configure.html",
|
||||
{
|
||||
"request": request,
|
||||
"user_data": user_data.model_dump(),
|
||||
"catalogs": zip(const.CATALOG_ID_DATA, const.CATALOG_NAME_DATA),
|
||||
"catalogs": sorted_catalogs,
|
||||
"resolutions": const.RESOLUTIONS,
|
||||
"sorting_options": const.TORRENT_SORTING_PRIORITY,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -95,6 +95,9 @@ class UserData(BaseModel):
|
||||
selected_resolutions: list[str | None] = Field(default=const.RESOLUTIONS)
|
||||
enable_catalogs: bool = True
|
||||
max_size: int | str | float = math.inf
|
||||
max_streams_per_resolution: int = 3
|
||||
show_full_torrent_name: bool = False
|
||||
torrent_sorting_priority: list[str] = Field(default=const.TORRENT_SORTING_PRIORITY)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_selected_resolutions(self) -> "UserData":
|
||||
@@ -118,6 +121,13 @@ class UserData(BaseModel):
|
||||
return int(v)
|
||||
raise ValueError("Invalid max_size")
|
||||
|
||||
@field_validator("torrent_sorting_priority", mode="after")
|
||||
def validate_torrent_sorting_priority(cls, v):
|
||||
for priority in v:
|
||||
if priority not in const.TORRENT_SORTING_PRIORITY:
|
||||
raise ValueError("Invalid priority")
|
||||
return v
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
@@ -107,8 +107,8 @@
|
||||
<hr class="section-divider">
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>Select Catalogs: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Select the types of content you wish to see in your Stremio's catalog. Uncheck to hide specific categories."></span></h6>
|
||||
<h6>Select & Arrange Catalogs: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Select and arrange the catalogs that you want to display in Stremio."></span></h6>
|
||||
<div id="catalogs" class="row">
|
||||
{% for catalog in catalogs %}
|
||||
<div class="col-12 col-md-6 col-lg-4 draggable-catalog" data-id="{{ catalog[0] }}">
|
||||
@@ -153,7 +153,9 @@
|
||||
|
||||
<!-- Streaming Filter Configuration -->
|
||||
<div class="section-container">
|
||||
<h4 class="section-header">Streaming Filters</h4>
|
||||
<h4 class="section-header">Streaming Preferences <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Customize how streams are sorted, limit results, and choose torrent display options to tailor your streaming experience in Stremio."></span>
|
||||
</h4>
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Select Streaming Resolutions -->
|
||||
@@ -179,8 +181,8 @@
|
||||
|
||||
<!-- File Size Range Filter -->
|
||||
<div class="mb-3">
|
||||
<h6>Select File Size Filter: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Select the file size range for the streams. Slide to the end for no limit."></span></h6>
|
||||
<h6>Set File Size Filter: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Select the file size range for the streams. Slide to the end for no limit."></span></h6>
|
||||
<!-- Slider for the file size -->
|
||||
<input type="range" class="form-range" id="max_size_slider" name="size_slider" min="0" max="21000000000"
|
||||
value="{{ user_data.max_size if user_data.max_size < 21000000000 else 21000000000 }}" step="1000000">
|
||||
@@ -189,6 +191,56 @@
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Stream Sorting Priority -->
|
||||
<div class="mb-3">
|
||||
<h6>Select & Arrange Sorting Priority: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Select and arrange the sorting options that you want to display in Stremio."></span></h6>
|
||||
|
||||
<div id="streamSortOrder" class="row">
|
||||
{% for sorting_option in sorting_options %}
|
||||
<div class="col-12 col-md-6 col-lg-4 sortable-list" data-id="{{ sorting_option }}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="selected_sorting_options"
|
||||
value="{{ sorting_option }}"
|
||||
id="sorting_{{ sorting_option }}"
|
||||
{% if sorting_option in user_data.torrent_sorting_priority %}checked{% endif %}>
|
||||
<label class="form-check-label" for="sorting_{{ sorting_option }}">
|
||||
{{ sorting_option.replace('_', ' ').title() }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Maximum Streams Result per Resolution Configuration -->
|
||||
<div class="mb-3">
|
||||
<label for="maxStreamsPerResolution">Max Streams Per Resolution: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Enter the maximum number of streams per resolution to display in Stremio."></span></label>
|
||||
<input type="number" class="form-control" id="maxStreamsPerResolution" name="maxStreamsPerResolution" min="1" placeholder="Enter maximum streams per resolution"
|
||||
value="{{ user_data.max_streams_per_resolution }}">
|
||||
</div>
|
||||
|
||||
<!-- Torrent Information Display Configuration -->
|
||||
<div class="mb-3">
|
||||
<h6>Torrent Stream Display option: <span class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Choose how you want to display the torrent information in Stremio."></span></h6>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="torrentDisplayOption" id="showParsedTorrentData" value="parsedData" {% if not user_data.show_full_torrent_name
|
||||
%}checked{% endif %}>
|
||||
<label class="form-check-label" for="showParsedTorrentData">
|
||||
Show Parsed Data (Quality, Resolution, Codec, Audio.)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="torrentDisplayOption" id="showTorrentName" value="fullName" {% if user_data.show_full_torrent_name %}checked{% endif %}>
|
||||
<label class="form-check-label" for="showTorrentName">
|
||||
Show Torrent Full Name
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
|
||||
@@ -222,6 +222,7 @@ document.getElementById('configForm').addEventListener('submit', async function
|
||||
|
||||
const provider = document.getElementById('provider_service').value;
|
||||
let isValid = true;
|
||||
const maxStreamsPerResolution = document.getElementById('maxStreamsPerResolution').value;
|
||||
|
||||
const validateInput = (elementId, condition) => {
|
||||
const element = document.getElementById(elementId);
|
||||
@@ -243,6 +244,9 @@ document.getElementById('configForm').addEventListener('submit', async function
|
||||
validateInput('password', document.getElementById('password').value);
|
||||
}
|
||||
|
||||
// Validation for Max Streams Per Resolution
|
||||
validateInput('maxStreamsPerResolution', !isNaN(maxStreamsPerResolution) && maxStreamsPerResolution > 0);
|
||||
|
||||
if (isValid) {
|
||||
let streamingProviderData = {};
|
||||
if (provider) {
|
||||
@@ -268,6 +272,12 @@ document.getElementById('configForm').addEventListener('submit', async function
|
||||
// Check if the max size is set to the slider's max value, which we treat as 'infinity'
|
||||
const maxSizeBytes = maxSizeValue === maxSize ? 'inf' : maxSizeValue;
|
||||
|
||||
// Capturing data from the Stream Sorting Priority
|
||||
const selectedSortingOptions = Array.from(document.querySelectorAll('#streamSortOrder .form-check-input:checked')).map(el => el.value);
|
||||
|
||||
// Capturing the selected Torrent Display Option
|
||||
const torrentDisplayOption = document.querySelector('input[name="torrentDisplayOption"]:checked').value;
|
||||
|
||||
|
||||
const userData = {
|
||||
streaming_provider: streamingProviderData,
|
||||
@@ -275,6 +285,9 @@ document.getElementById('configForm').addEventListener('submit', async function
|
||||
selected_resolutions: Array.from(document.querySelectorAll('input[name="selected_resolutions"]:checked')).map(el => el.value),
|
||||
enable_catalogs: document.getElementById('enable_catalogs').checked,
|
||||
max_size: maxSizeBytes,
|
||||
max_streams_per_resolution: maxStreamsPerResolution,
|
||||
torrent_sorting_priority: selectedSortingOptions,
|
||||
show_full_torrent_name: torrentDisplayOption === 'fullName',
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -317,4 +330,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
ghostClass: 'sortable-ghost', // Class for the ghost element
|
||||
dragClass: 'sortable-drag', // Class applied to the element being dragged
|
||||
});
|
||||
new Sortable(document.getElementById('streamSortOrder'), {
|
||||
handle: '.sortable-list',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'sortable-drag',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,3 +100,5 @@ NO_CACHE_HEADERS = {
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
}
|
||||
|
||||
TORRENT_SORTING_PRIORITY = ["cached", "size", "seeders", "created_at"]
|
||||
|
||||
+96
-57
@@ -46,49 +46,83 @@ ADULT_CONTENT_KEYWORDS = re.compile(
|
||||
settings.adult_content_regex_keywords,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# Define provider-specific cache update functions
|
||||
CACHE_UPDATE_FUNCTIONS = {
|
||||
"alldebrid": update_ad_cache_status,
|
||||
"debridlink": update_dl_cache_status,
|
||||
"offcloud": update_oc_cache_status,
|
||||
"pikpak": update_pikpak_cache_status,
|
||||
"realdebrid": update_rd_cache_status,
|
||||
"seedr": update_seedr_cache_status,
|
||||
"torbox": update_torbox_cache_status,
|
||||
"premiumize": update_pm_cache_status,
|
||||
}
|
||||
|
||||
# Define provider-specific downloaded info hashes fetch functions
|
||||
FETCH_DOWNLOADED_INFO_HASHES_FUNCTIONS = {
|
||||
"alldebrid": fetch_downloaded_info_hashes_from_ad,
|
||||
"debridlink": fetch_downloaded_info_hashes_from_dl,
|
||||
"offcloud": fetch_downloaded_info_hashes_from_oc,
|
||||
"pikpak": fetch_downloaded_info_hashes_from_pikpak,
|
||||
"realdebrid": fetch_downloaded_info_hashes_from_rd,
|
||||
"seedr": fetch_downloaded_info_hashes_from_seedr,
|
||||
"torbox": fetch_downloaded_info_hashes_from_torbox,
|
||||
"premiumize": fetch_downloaded_info_hashes_from_premiumize,
|
||||
}
|
||||
|
||||
|
||||
async def filter_and_sort_streams(
|
||||
streams: list[TorrentStreams], user_data: UserData
|
||||
) -> list[TorrentStreams]:
|
||||
# Filter streams by selected catalogs and resolutions
|
||||
# Convert to sets for faster lookups
|
||||
selected_catalogs_set = set(user_data.selected_catalogs)
|
||||
selected_resolutions_set = set(user_data.selected_resolutions)
|
||||
|
||||
# Step 1: Filter streams by selected catalogs, resolutions, and size
|
||||
filtered_streams = [
|
||||
stream
|
||||
for stream in streams
|
||||
if any(catalog in stream.catalog for catalog in user_data.selected_catalogs)
|
||||
and stream.resolution in user_data.selected_resolutions
|
||||
if any(catalog_id in selected_catalogs_set for catalog_id in stream.catalog)
|
||||
and stream.resolution in selected_resolutions_set
|
||||
and stream.size <= user_data.max_size
|
||||
]
|
||||
|
||||
if not filtered_streams:
|
||||
return []
|
||||
|
||||
# Define provider-specific cache update functions
|
||||
cache_update_functions = {
|
||||
"alldebrid": update_ad_cache_status,
|
||||
"debridlink": update_dl_cache_status,
|
||||
"offcloud": update_oc_cache_status,
|
||||
"pikpak": update_pikpak_cache_status,
|
||||
"realdebrid": update_rd_cache_status,
|
||||
"seedr": update_seedr_cache_status,
|
||||
"torbox": update_torbox_cache_status,
|
||||
"premiumize": update_pm_cache_status,
|
||||
}
|
||||
|
||||
# Update cache status based on provider
|
||||
if user_data.streaming_provider:
|
||||
if cache_update_function := cache_update_functions.get(
|
||||
user_data.streaming_provider.service
|
||||
):
|
||||
if asyncio.iscoroutinefunction(cache_update_function):
|
||||
await cache_update_function(streams, user_data)
|
||||
else:
|
||||
await asyncio.to_thread(cache_update_function, streams, user_data)
|
||||
|
||||
# Sort streams by cache status, creation date, and size
|
||||
return sorted(
|
||||
filtered_streams, key=lambda x: (x.cached, x.size, x.created_at), reverse=True
|
||||
# Step 2: Update cache status based on provider
|
||||
cache_update_function = CACHE_UPDATE_FUNCTIONS.get(
|
||||
user_data.streaming_provider.service
|
||||
if user_data.streaming_provider
|
||||
else "torrent"
|
||||
)
|
||||
if cache_update_function:
|
||||
if asyncio.iscoroutinefunction(cache_update_function):
|
||||
await cache_update_function(filtered_streams, user_data)
|
||||
else:
|
||||
await asyncio.to_thread(cache_update_function, filtered_streams, user_data)
|
||||
|
||||
# Step 3: Dynamically sort streams based on user preferences
|
||||
def dynamic_sort_key(stream):
|
||||
return tuple(
|
||||
(getattr(stream, sort_key) if getattr(stream, sort_key) is not None else 0)
|
||||
for sort_key in user_data.torrent_sorting_priority
|
||||
)
|
||||
|
||||
dynamically_sorted_streams = sorted(
|
||||
filtered_streams, key=dynamic_sort_key, reverse=True
|
||||
)
|
||||
|
||||
# Step 4: Limit streams per resolution based on user preference, after dynamic sorting
|
||||
limited_streams = []
|
||||
streams_count_per_resolution = {}
|
||||
for stream in dynamically_sorted_streams:
|
||||
count = streams_count_per_resolution.get(stream.resolution, 0)
|
||||
if count < user_data.max_streams_per_resolution:
|
||||
limited_streams.append(stream)
|
||||
streams_count_per_resolution[stream.resolution] = count + 1
|
||||
|
||||
return limited_streams
|
||||
|
||||
|
||||
async def parse_stream_data(
|
||||
@@ -102,7 +136,26 @@ async def parse_stream_data(
|
||||
|
||||
streams = await filter_and_sort_streams(streams, user_data)
|
||||
|
||||
# Pre-determined values
|
||||
show_full_torrent_name = user_data.show_full_torrent_name
|
||||
has_streaming_provider = user_data.streaming_provider is not None
|
||||
streaming_provider_name = (
|
||||
user_data.streaming_provider.service.title()
|
||||
if has_streaming_provider
|
||||
else "Torrent"
|
||||
)
|
||||
base_proxy_url_template = (
|
||||
f"{settings.host_url}/streaming_provider/{secret_str}/stream?info_hash={{}}"
|
||||
if has_streaming_provider
|
||||
else None
|
||||
)
|
||||
|
||||
for stream_data in streams:
|
||||
torrent_name = (
|
||||
stream_data.torrent_name.replace(".torrent", "").replace(".", " ")
|
||||
if show_full_torrent_name
|
||||
else None
|
||||
)
|
||||
quality_detail = " - ".join(
|
||||
filter(
|
||||
None,
|
||||
@@ -116,20 +169,17 @@ async def parse_stream_data(
|
||||
)
|
||||
|
||||
episode_data = stream_data.get_episode(season, episode)
|
||||
|
||||
if user_data.streaming_provider:
|
||||
streaming_provider = user_data.streaming_provider.service.title()
|
||||
if stream_data.cached:
|
||||
streaming_provider += " ⚡️"
|
||||
else:
|
||||
streaming_provider += " ⏳"
|
||||
else:
|
||||
streaming_provider = "Torrent ⏳"
|
||||
|
||||
seeders = f"👤 {stream_data.seeders}" if stream_data.seeders else None
|
||||
streaming_provider = (
|
||||
f"{streaming_provider_name} ⚡️"
|
||||
if stream_data.cached
|
||||
else f"{streaming_provider_name} ⏳"
|
||||
)
|
||||
seeders = (
|
||||
f"👤 {stream_data.seeders}" if stream_data.seeders is not None else None
|
||||
)
|
||||
|
||||
description_parts = [
|
||||
quality_detail,
|
||||
torrent_name or quality_detail,
|
||||
convert_bytes_to_readable(
|
||||
episode_data.size or stream_data.size
|
||||
if episode_data
|
||||
@@ -151,13 +201,13 @@ async def parse_stream_data(
|
||||
"behaviorHints": {"bingeGroup": f"MediaFusion-{quality_detail}"},
|
||||
}
|
||||
|
||||
if user_data.streaming_provider:
|
||||
base_proxy_url = f"{settings.host_url}/streaming_provider/{secret_str}/stream?info_hash={stream_data.id}"
|
||||
if has_streaming_provider:
|
||||
base_proxy_url = base_proxy_url_template.format(stream_data.id)
|
||||
if episode_data:
|
||||
base_proxy_url += f"&season={season}&episode={episode}"
|
||||
stream_details["url"] = base_proxy_url
|
||||
stream_details.pop("infoHash")
|
||||
stream_details.pop("fileIdx")
|
||||
stream_details.pop("infoHash", None)
|
||||
stream_details.pop("fileIdx", None)
|
||||
stream_details["behaviorHints"]["notWebReady"] = True
|
||||
|
||||
stream_list.append(Stream(**stream_details))
|
||||
@@ -170,7 +220,7 @@ def convert_bytes_to_readable(size_bytes: int) -> str:
|
||||
Convert a size in bytes into a more human-readable format.
|
||||
"""
|
||||
if not size_bytes:
|
||||
return "0B"
|
||||
return ""
|
||||
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
@@ -258,18 +308,7 @@ def parse_tv_stream_data(tv_data: MediaFusionTVMetaData) -> list[Stream]:
|
||||
|
||||
|
||||
async def fetch_downloaded_info_hashes(user_data: UserData) -> list[str]:
|
||||
fetch_downloaded_info_hashes_functions = {
|
||||
"alldebrid": fetch_downloaded_info_hashes_from_ad,
|
||||
"debridlink": fetch_downloaded_info_hashes_from_dl,
|
||||
"offcloud": fetch_downloaded_info_hashes_from_oc,
|
||||
"pikpak": fetch_downloaded_info_hashes_from_pikpak,
|
||||
"realdebrid": fetch_downloaded_info_hashes_from_rd,
|
||||
"seedr": fetch_downloaded_info_hashes_from_seedr,
|
||||
"torbox": fetch_downloaded_info_hashes_from_torbox,
|
||||
"premiumize": fetch_downloaded_info_hashes_from_premiumize,
|
||||
}
|
||||
|
||||
if fetch_downloaded_info_hashes_function := fetch_downloaded_info_hashes_functions.get(
|
||||
if fetch_downloaded_info_hashes_function := FETCH_DOWNLOADED_INFO_HASHES_FUNCTIONS.get(
|
||||
user_data.streaming_provider.service
|
||||
):
|
||||
if asyncio.iscoroutinefunction(fetch_downloaded_info_hashes_function):
|
||||
|
||||
Reference in New Issue
Block a user