#102: Add option to dynamic sorting, max streams per resolution

#113: Add option to display full torrent name
This commit is contained in:
mhdzumair
2024-02-16 22:30:11 +05:30
parent 380739564e
commit df1ba3f5bc
6 changed files with 194 additions and 63 deletions
+10 -1
View File
@@ -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,
},
)
+10
View File
@@ -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"
+57 -5
View File
@@ -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 -->
+19
View File
@@ -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',
});
});
+2
View File
@@ -100,3 +100,5 @@ NO_CACHE_HEADERS = {
"Pragma": "no-cache",
"Expires": "0",
}
TORRENT_SORTING_PRIORITY = ["cached", "size", "seeders", "created_at"]
+96 -57
View File
@@ -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):