Skip to content

Commit

Permalink
Merge pull request #13 from z1nc0r3/more-plugin-settings
Browse files Browse the repository at this point in the history
Feat: Introduce more plugin settings (sorting, preferred format, etc)
  • Loading branch information
z1nc0r3 authored Nov 4, 2024
2 parents cbbe574 + bfa5f87 commit 8cf390e
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 31 deletions.
46 changes: 41 additions & 5 deletions SettingsTemplate.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
body:
- type: input
attributes:
name: download_path
label: 'Download path:'
defaultValue: "%USERPROFILE%\\Downloads"
- type: input
attributes:
name: download_path
label: "Download path:"
description: "Enter the path where you want to save the downloaded files."
defaultValue: "%USERPROFILE%\\Downloads"
- type: dropdown
attributes:
name: sorting_order
label: "Sorting order:"
defaultValue: "Resolution"
description: "Select the sorting order for the results."
options:
- Resolution
- File Size
- Total Bit Rate
- FPS
- type: dropdown
attributes:
name: preferred_video_format
label: "Preferred Video format:"
defaultValue: "mp4"
description: "Select the preferred video format for downloading videos."
options:
- mp4
- mkv
- avi
- flv
- webm
- type: dropdown
attributes:
name: preferred_audio_format
label: "Preferred Audio format:"
defaultValue: "mp3"
description: "Select the preferred audio format for downloading audio."
options:
- mp3
- m4a
- wav
- flac

174 changes: 155 additions & 19 deletions plugin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
from datetime import datetime, timedelta
from pathlib import Path
from typing import Tuple

from pyflowlauncher import Plugin, ResultResponse, send_results
from pyflowlauncher.settings import settings
Expand All @@ -22,30 +23,129 @@
EXE_PATH = os.path.join(os.path.dirname(__file__), "yt-dlp.exe")
CHECK_INTERVAL_DAYS = 10
DEFAULT_DOWNLOAD_PATH = str(Path.home() / "Downloads")
URL_REGEX = (
"((http|https)://)(www.)?"
+ "[a-zA-Z0-9@:%._\\+~#?&//=]"
+ "{1,256}\\.[a-z]"
+ "{2,6}\\b([-a-zA-Z0-9@:%"
+ "._\\+~#?&//=]*)"
)

plugin = Plugin()


def is_valid_url(url: str) -> bool:
regex = (
"((http|https)://)(www.)?"
+ "[a-zA-Z0-9@:%._\\+~#?&//=]"
+ "{1,256}\\.[a-z]"
+ "{2,6}\\b([-a-zA-Z0-9@:%"
+ "._\\+~#?&//=]*)"
"""
Check if the given URL is valid based on a predefined regex pattern.
Args:
url (str): The URL string to validate.
Returns:
bool: True if the URL matches the regex pattern, False otherwise.
"""

return bool(re.match(URL_REGEX, url))


def fetch_settings() -> Tuple[str, str, str, str]:
"""
Fetches the user settings for the plugin.
Returns:
Tuple[str, str, str, str]: A tuple containing:
- download_path (str): The path where videos will be downloaded.
- sorting_order (str): The order in which videos will be sorted (default is "Resolution").
- pref_video_format (str): The preferred video format (default is "mp4").
- pref_audio_format (str): The preferred audio format (default is "mp3").
"""
try:
download_path = settings().get("download_path") or DEFAULT_DOWNLOAD_PATH
if not os.path.exists(download_path):
download_path = DEFAULT_DOWNLOAD_PATH

sorting_order = settings().get("sorting_order") or "Resolution"
pref_video_format = settings().get("preferred_video_format") or "mp4"
pref_audio_format = settings().get("preferred_audio_format") or "mp3"
except Exception as _:
download_path = DEFAULT_DOWNLOAD_PATH
sorting_order = "Resolution"
pref_video_format = "mp4"
pref_audio_format = "mp3"

return download_path, sorting_order, pref_video_format, pref_audio_format


def sort_by_resolution(formats):
"""
Sorts a list of video formats by their resolution in descending order.
Returns:
list of dict: The list of video formats sorted by resolution in descending order.
Audio-only formats are considered to have the lowest resolution.
"""

def resolution_to_tuple(resolution):
if resolution == "audio only":
return (0, 0)
return tuple(map(int, resolution.split("x")))

return sorted(
formats, key=lambda x: resolution_to_tuple(x["resolution"]), reverse=True
)

return bool(re.match(regex, url))

def sort_by_tbr(formats):
"""
Sort a list of video formats by their 'tbr' (total bitrate) in descending order.
Returns:
list: The input list sorted by the 'tbr' value in descending order.
"""
return sorted(formats, key=lambda x: x["tbr"], reverse=True)


def sort_by_fps(formats):
"""
Sort a list of video formats by frames per second (fps) in descending order.
Returns:
list of dict: The list of video formats sorted by fps in descending order.
Formats with None fps values are placed at the end.
"""
return sorted(
formats,
key=lambda x: (
x["fps"] is None,
-x["fps"] if x["fps"] is not None else float("-inf"),
),
)


def sort_by_size(formats):
"""
Sort a list of video formats by their file size in descending order.
Returns:
list of dict: The list of video formats sorted by file size in
descending order. Formats with no file size information
(None) are placed at the end of the list.
"""
return sorted(
formats,
key=lambda x: (
x["filesize"] is None,
-x["filesize"] if x["filesize"] is not None else float("-inf"),
),
)


@plugin.on_method
def query(query: str) -> ResultResponse:
download_path = settings().get("download_path") or DEFAULT_DOWNLOAD_PATH
if not os.path.exists(download_path):
download_path = DEFAULT_DOWNLOAD_PATH
d_path, sort, pvf, paf = fetch_settings()

if not query.strip():
return send_results([init_results(download_path)])
return send_results([init_results(d_path)])

if not is_valid_url(query):
return send_results([invalid_result()])
Expand All @@ -55,7 +155,6 @@ def query(query: str) -> ResultResponse:
ydl_opts = {
"quiet": True,
"no_warnings": True,
"format-sort": "res,tbr",
}
ydl = CustomYoutubeDL(params=ydl_opts)
info = ydl.extract_info(query)
Expand All @@ -64,32 +163,69 @@ def query(query: str) -> ResultResponse:
return send_results([error_result()])

formats = [
format
{
"format_id": format["format_id"],
"resolution": format.get("resolution"),
"filesize": format.get("filesize"),
"tbr": format.get("tbr"),
"fps": format.get("fps"),
}
for format in info["formats"]
if format.get("resolution") and format.get("tbr")
]

if not formats:
return send_results([empty_result()])

if sort == "Resolution":
formats = sort_by_resolution(formats)
elif sort == "File Size":
formats = sort_by_size(formats)
elif sort == "Total Bit Rate":
formats = sort_by_tbr(formats)
elif sort == "FPS":
formats = sort_by_fps(formats)

results = [
query_result(query, info, format, download_path) for format in reversed(formats)
query_result(
query,
str(info.get("thumbnail")),
str(info.get("title")),
format,
d_path,
pvf,
paf,
)
for format in formats
]
return send_results(results)


@plugin.on_method
def download(url: str, format_id: str, download_path: str) -> None:
def download(
url: str,
format_id: str,
download_path: str,
pref_video_path: str,
pref_audio_path: str,
is_audio: bool,
) -> None:
last_modified_time = datetime.fromtimestamp(os.path.getmtime(EXE_PATH))

base_command = f'yt-dlp "{url}" -f {format_id}+ba -P {download_path} --windows-filenames --restrict-filenames --trim-filenames 50 --quiet --progress --no-mtime --force-overwrites --no-part'
format = (
f"-f ba -x --audio-format {pref_audio_path}"
if is_audio
else f"-f {format_id}+ba --remux-video {pref_video_path}"
)

command = (
f"{base_command} -U"
update = (
f"-U"
if datetime.now() - last_modified_time >= timedelta(days=CHECK_INTERVAL_DAYS)
else base_command
else ""
)

command = f'yt-dlp "{url}" {format} -P {download_path} --windows-filenames --restrict-filenames --trim-filenames 50 --quiet --progress --no-mtime --force-overwrites --no-part {update}'

os.system(command)


Expand Down
19 changes: 14 additions & 5 deletions plugin/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,22 @@ def empty_result() -> Result:
return Result(Title="Couldn't find any video formats.", IcoPath="Images/error.png")


def query_result(query, info, format, download_path) -> Result:
def query_result(
query, thumbnail, title, format, download_path, pref_video_path, pref_audio_path
) -> Result:
return Result(
Title=info["title"],
SubTitle=f"{format['resolution']} ({round(format['tbr'])} kbps) {'┃ Format: ' + str(format['ext']) if format.get('ext') else ''} {'┃ FPS: ' + str(format['fps']) if format.get('fps') else ''}",
IcoPath=info.get("thumbnail") or "Images/app.png",
Title=title,
SubTitle=f"Res: {format['resolution']} ({round(format['tbr'], 2)} kbps) {'┃ Size: ' + str(round(format['filesize'] / 1024 / 1024, 2)) + 'MB' if format.get('filesize') else ''} {'┃ FPS: ' + str(int(format['fps'])) if format.get('fps') else ''}",
IcoPath=thumbnail or "Images/app.png",
JsonRPCAction={
"method": "download",
"parameters": [query, f"{format['format_id']}", download_path],
"parameters": [
query,
f"{format['format_id']}",
download_path,
pref_video_path,
pref_audio_path,
format["resolution"] == "audio only",
],
},
)
3 changes: 1 addition & 2 deletions plugin/ytdlp.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from yt_dlp import YoutubeDL


# Custom YoutubeDL class to exception handling
""" CustomYoutubeDL class for better error handling """
class CustomYoutubeDL(YoutubeDL):
def __init__(self, params=None, auto_init=True):
super().__init__(params, auto_init)
Expand Down

0 comments on commit 8cf390e

Please sign in to comment.