Merge pull request #525 from arabcoders/dev
This commit is contained in:
commit
714b6d95ba
10 changed files with 335 additions and 99 deletions
|
|
@ -85,14 +85,14 @@ class YTDLP(yt_dlp.YoutubeDL):
|
|||
if not (archive_id := self._make_archive_id(info_dict)):
|
||||
return
|
||||
|
||||
assert archive_id
|
||||
assert archive_id, "Expected non-empty archive ID"
|
||||
|
||||
self.write_debug(f"Adding to archive: {archive_id}")
|
||||
self.archive.add(archive_id)
|
||||
old_archive_ids = info_dict.get("_old_archive_ids", [])
|
||||
if old_archive_ids and isinstance(old_archive_ids, list) and len(old_archive_ids) > 0:
|
||||
for old_id in old_archive_ids:
|
||||
if old_id == archive_id or not old_id.startswith("generic "):
|
||||
if old_id == archive_id:
|
||||
continue
|
||||
|
||||
self.write_debug(f"Adding to archive (old id): {old_id}")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from app.library.Utils import REMOVE_KEYS
|
||||
from app.library.ytdlp import _ArchiveProxy, ytdlp_options
|
||||
from app.library.ytdlp import YTDLP, _ArchiveProxy, ytdlp_options
|
||||
|
||||
|
||||
class TestArchiveProxy:
|
||||
|
|
@ -79,3 +79,200 @@ class TestYtDlpOptions:
|
|||
# We expect at least one to be present (e.g., -P / --paths, etc.)
|
||||
assert len(present_ignored_flags) > 0
|
||||
assert all(flag_to_ignored[f] is True for f in present_ignored_flags)
|
||||
|
||||
|
||||
class TestYTDLP:
|
||||
"""Test the YTDLP class overridden methods."""
|
||||
|
||||
def _create_ytdlp(self, params=None):
|
||||
"""Helper to create a YTDLP instance with mocked parent __init__."""
|
||||
with patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None):
|
||||
ytdlp = YTDLP(params=params)
|
||||
ytdlp.params = params or {}
|
||||
return ytdlp
|
||||
|
||||
@patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__")
|
||||
def test_init_patches_download_archive_param(self, mock_super_init) -> None:
|
||||
"""Test that __init__ removes download_archive before calling super, then restores it."""
|
||||
mock_super_init.return_value = None
|
||||
|
||||
params = {"download_archive": "/tmp/archive.txt", "quiet": True}
|
||||
|
||||
ytdlp = YTDLP(params=params, auto_init=True)
|
||||
# Set params as it would be after super().__init__
|
||||
ytdlp.params = {}
|
||||
|
||||
# Verify super().__init__ was called without download_archive
|
||||
mock_super_init.assert_called_once()
|
||||
call_kwargs = mock_super_init.call_args[1]
|
||||
assert "download_archive" not in call_kwargs["params"]
|
||||
assert call_kwargs["params"]["quiet"] is True
|
||||
assert call_kwargs["auto_init"] is True
|
||||
|
||||
# Our __init__ code manually sets these after super()
|
||||
ytdlp.params["download_archive"] = "/tmp/archive.txt"
|
||||
|
||||
# Verify download_archive was restored to params
|
||||
assert ytdlp.params["download_archive"] == "/tmp/archive.txt"
|
||||
|
||||
# Verify archive proxy was set up
|
||||
assert isinstance(ytdlp.archive, _ArchiveProxy)
|
||||
assert ytdlp.archive._file == "/tmp/archive.txt"
|
||||
|
||||
@patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__")
|
||||
def test_init_handles_no_download_archive(self, mock_super_init) -> None:
|
||||
"""Test __init__ works correctly when download_archive is not in params."""
|
||||
mock_super_init.return_value = None
|
||||
|
||||
params = {"quiet": True}
|
||||
|
||||
ytdlp = YTDLP(params=params, auto_init=False)
|
||||
|
||||
# Verify super().__init__ was called with original params
|
||||
mock_super_init.assert_called_once()
|
||||
call_kwargs = mock_super_init.call_args[1]
|
||||
assert call_kwargs["params"]["quiet"] is True
|
||||
assert call_kwargs["auto_init"] is False
|
||||
|
||||
# Verify archive proxy is falsey
|
||||
assert isinstance(ytdlp.archive, _ArchiveProxy)
|
||||
assert not ytdlp.archive
|
||||
|
||||
@patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__")
|
||||
def test_init_handles_none_params(self, mock_super_init) -> None:
|
||||
"""Test __init__ handles None params gracefully."""
|
||||
mock_super_init.return_value = None
|
||||
|
||||
ytdlp = YTDLP(params=None)
|
||||
|
||||
mock_super_init.assert_called_once_with(params=None, auto_init=True)
|
||||
assert isinstance(ytdlp.archive, _ArchiveProxy)
|
||||
assert not ytdlp.archive
|
||||
|
||||
@patch("app.library.ytdlp.yt_dlp.YoutubeDL._delete_downloaded_files")
|
||||
def test_delete_downloaded_files_skips_when_interrupted(self, mock_super_delete) -> None:
|
||||
"""Test _delete_downloaded_files skips cleanup when _interrupted is True."""
|
||||
with patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None):
|
||||
ytdlp = YTDLP(params={})
|
||||
ytdlp.to_screen = Mock()
|
||||
|
||||
# Set interrupted flag
|
||||
ytdlp._interrupted = True
|
||||
|
||||
result = ytdlp._delete_downloaded_files("arg1", "arg2", kwarg1="value1")
|
||||
|
||||
# Should not call super method
|
||||
mock_super_delete.assert_not_called()
|
||||
# Should show message
|
||||
ytdlp.to_screen.assert_called_once_with("[info] Cancelled — skipping temp cleanup.")
|
||||
# Should return None
|
||||
assert result is None
|
||||
|
||||
@patch("app.library.ytdlp.yt_dlp.YoutubeDL._delete_downloaded_files")
|
||||
def test_delete_downloaded_files_calls_super_when_not_interrupted(self, mock_super_delete) -> None:
|
||||
"""Test _delete_downloaded_files calls super when not interrupted."""
|
||||
mock_super_delete.return_value = "cleanup_result"
|
||||
|
||||
with patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None):
|
||||
ytdlp = YTDLP(params={})
|
||||
ytdlp._interrupted = False
|
||||
|
||||
result = ytdlp._delete_downloaded_files("arg1", kwarg1="value1")
|
||||
|
||||
# Should call super method with same args
|
||||
mock_super_delete.assert_called_once_with("arg1", kwarg1="value1")
|
||||
# Should return super's result
|
||||
assert result == "cleanup_result"
|
||||
|
||||
def test_record_download_archive_does_nothing_without_download_archive_param(self) -> None:
|
||||
"""Test record_download_archive returns early when download_archive is not set."""
|
||||
ytdlp = self._create_ytdlp(params={})
|
||||
ytdlp.archive = Mock()
|
||||
|
||||
ytdlp.record_download_archive({"id": "test123"})
|
||||
|
||||
# Should not interact with archive
|
||||
ytdlp.archive.add.assert_not_called()
|
||||
|
||||
def test_record_download_archive_adds_archive_id(self) -> None:
|
||||
"""Test record_download_archive adds the archive ID."""
|
||||
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
|
||||
ytdlp.write_debug = Mock()
|
||||
ytdlp.archive = Mock()
|
||||
ytdlp._make_archive_id = Mock(return_value="youtube test123")
|
||||
|
||||
info_dict = {"id": "test123", "ie_key": "Youtube"}
|
||||
|
||||
ytdlp.record_download_archive(info_dict)
|
||||
|
||||
# Verify _make_archive_id was called
|
||||
ytdlp._make_archive_id.assert_called_once_with(info_dict)
|
||||
|
||||
# Verify archive.add was called with the archive_id
|
||||
ytdlp.archive.add.assert_called_once_with("youtube test123")
|
||||
ytdlp.write_debug.assert_called_with("Adding to archive: youtube test123")
|
||||
|
||||
def test_record_download_archive_handles_old_archive_ids(self) -> None:
|
||||
"""Test record_download_archive adds _old_archive_ids when present."""
|
||||
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
|
||||
ytdlp.write_debug = Mock()
|
||||
ytdlp.archive = Mock()
|
||||
ytdlp._make_archive_id = Mock(return_value="youtube new123")
|
||||
|
||||
info_dict = {
|
||||
"id": "new123",
|
||||
"ie_key": "Youtube",
|
||||
"_old_archive_ids": ["youtube old123", "youtube old456", "youtube new123"],
|
||||
}
|
||||
|
||||
ytdlp.record_download_archive(info_dict)
|
||||
|
||||
# Should add main archive_id
|
||||
assert ytdlp.archive.add.call_count == 3
|
||||
calls = [call[0][0] for call in ytdlp.archive.add.call_args_list]
|
||||
|
||||
# First call is main archive_id
|
||||
assert calls[0] == "youtube new123"
|
||||
|
||||
# Should add old IDs except the duplicate
|
||||
assert "youtube old123" in calls
|
||||
assert "youtube old456" in calls
|
||||
# Should not add duplicate (youtube new123 appears only once)
|
||||
assert calls.count("youtube new123") == 1
|
||||
|
||||
def test_record_download_archive_skips_empty_old_archive_ids(self) -> None:
|
||||
"""Test record_download_archive handles empty or invalid _old_archive_ids."""
|
||||
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
|
||||
ytdlp.write_debug = Mock()
|
||||
ytdlp.archive = Mock()
|
||||
ytdlp._make_archive_id = Mock(return_value="youtube test123")
|
||||
|
||||
# Test with empty list
|
||||
info_dict = {"id": "test123", "ie_key": "Youtube", "_old_archive_ids": []}
|
||||
ytdlp.record_download_archive(info_dict)
|
||||
assert ytdlp.archive.add.call_count == 1 # Only main ID
|
||||
|
||||
ytdlp.archive.reset_mock()
|
||||
|
||||
# Test with None
|
||||
info_dict = {"id": "test123", "ie_key": "Youtube", "_old_archive_ids": None}
|
||||
ytdlp.record_download_archive(info_dict)
|
||||
assert ytdlp.archive.add.call_count == 1 # Only main ID
|
||||
|
||||
ytdlp.archive.reset_mock()
|
||||
|
||||
# Test with non-list value
|
||||
info_dict = {"id": "test123", "ie_key": "Youtube", "_old_archive_ids": "not a list"}
|
||||
ytdlp.record_download_archive(info_dict)
|
||||
assert ytdlp.archive.add.call_count == 1 # Only main ID
|
||||
|
||||
def test_record_download_archive_returns_early_on_empty_archive_id(self) -> None:
|
||||
"""Test record_download_archive returns early when _make_archive_id returns empty."""
|
||||
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
|
||||
ytdlp.archive = Mock()
|
||||
ytdlp._make_archive_id = Mock(return_value=None)
|
||||
|
||||
ytdlp.record_download_archive({"id": "test123"})
|
||||
|
||||
# Should not add anything
|
||||
ytdlp.archive.add.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<span class="icon"><i class="fa-solid fa-link" /></span>
|
||||
<span class="has-tooltip" v-tooltip="'Use Shift+Enter to switch to multiline input mode.'">
|
||||
URLs separated by newlines or <span class="is-bold is-lowercase">{{ getSeparatorsName(separator)
|
||||
}}</span>
|
||||
}}</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="field is-grouped">
|
||||
|
|
@ -33,11 +33,14 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-4-tablet is-12-mobile">
|
||||
<label class="label" for="preset">
|
||||
<span class="icon"><i class="fa-solid fa-sliders" /></span>
|
||||
<span>Preset</span>
|
||||
</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control" @click="show_description = !show_description">
|
||||
<label class="button is-static">
|
||||
<span class="icon"><i class="fa-solid fa-sliders" /></span>
|
||||
<span>Preset</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
|
|
@ -62,22 +65,25 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field has-addons" v-tooltip="'Folder relative to ' + config.app.download_path">
|
||||
<div class="control">
|
||||
<label class="label" for="folder">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Download path</span>
|
||||
</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control" v-tooltip="`Download path relative to: ${config.app.download_path}`">
|
||||
<label class="button is-static">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
<span>Save in</span>
|
||||
<span>{{ shortPath(config.app.download_path) }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input type="text" class="input is-fullwidth" id="path" v-model="form.folder"
|
||||
<input type="text" class="input is-fullwidth" id="folder" v-model="form.folder"
|
||||
:placeholder="getDefault('folder', '/')" :disabled="!socket.isConnected || addInProgress"
|
||||
list="folders">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="column is-align-content-end">
|
||||
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
|
||||
:disabled="!socket.isConnected">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
|
|
@ -161,7 +167,7 @@
|
|||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use the <NuxtLink target="_blank"
|
||||
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
||||
Recommended addon</NuxtLink> by yt-dlp to export cookies. <span class="has-text-danger">The
|
||||
Recommended addon</NuxtLink> to export cookies. <span class="has-text-danger">The
|
||||
cookies MUST be in Netscape HTTP Cookie format.</span>
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -401,12 +407,6 @@ const splitUrls = (urlString: string): Array<string> => {
|
|||
}
|
||||
|
||||
const addDownload = async () => {
|
||||
if (' ' === form.value?.folder) {
|
||||
toast.warning('The download folder contain only spaces. Resetting to default.')
|
||||
form.value.folder = ''
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
if (form.value.folder) {
|
||||
form.value.folder = form.value.folder.trim()
|
||||
}
|
||||
|
|
@ -578,14 +578,9 @@ onMounted(async () => {
|
|||
form.value.preset = config.app.default_preset
|
||||
}
|
||||
|
||||
if (' ' === form.value?.folder) {
|
||||
toast.warning('The download folder contain only spaces. Resetting to default.')
|
||||
form.value.folder = ''
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
if (form.value.folder) {
|
||||
if (form.value?.folder) {
|
||||
form.value.folder = form.value.folder.trim()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
if (config.isLoaded() && form.value?.preset && !config.presets.some(p => p.name === form.value.preset)) {
|
||||
|
|
@ -620,6 +615,11 @@ const runCliCommand = async (): Promise<void> => {
|
|||
return
|
||||
}
|
||||
|
||||
if (form.value?.folder) {
|
||||
form.value.folder = form.value.folder.trim()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const { status } = await dialog.confirmDialog({
|
||||
title: 'Run CLI Command',
|
||||
message: `This will generate a yt-dlp command and run it in the console. Continue?`,
|
||||
|
|
@ -697,11 +697,13 @@ const testDownloadOptions = async (): Promise<void> => {
|
|||
return
|
||||
}
|
||||
|
||||
let form_cli = (form.value?.cli || '').trim()
|
||||
if (' ' === form.value?.folder) {
|
||||
form.value.folder = ''
|
||||
if (form.value?.folder) {
|
||||
form.value.folder = form.value.folder.trim()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
let form_cli = (form.value?.cli || '').trim()
|
||||
|
||||
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
|
||||
const joined = []
|
||||
for (const [key, value] of Object.entries(dlFields.value)) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<main class="columns mt-2 is-multiline">
|
||||
<div class="column is-12">
|
||||
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
|
||||
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block">
|
||||
|
|
@ -104,28 +104,32 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
Default Download path
|
||||
</label>
|
||||
<div class="control">
|
||||
<label class="label is-inline" for="folder">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
download path
|
||||
</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control" v-tooltip="`Full Path: ${config.app.download_path}`">
|
||||
<span class="button is-static">
|
||||
<span>{{ shortPath(config.app.download_path) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
|
||||
v-model="form.folder" :disabled="addInProgress" list="folders">
|
||||
</div>
|
||||
<span class="help is-bold">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use this folder if non is given with URL. Leave empty to use default download path. Default
|
||||
download path <code>{{ config.app.download_path }}</code>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="help is-bold">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use this defined path if non are given with the URL.</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="output_template">
|
||||
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||
Default Output template
|
||||
Output template
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="text" class="input" id="output_template" :disabled="addInProgress"
|
||||
|
|
@ -133,10 +137,7 @@
|
|||
</div>
|
||||
<span class="help is-bold">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use this output template if non are given with URL. if not set, it will defaults to
|
||||
<code>{{ config.app.output_template }}</code>. For more information visit <NuxtLink
|
||||
href="https://github.com/yt-dlp/yt-dlp#output-template" target="_blank">this page</NuxtLink>.
|
||||
</span>
|
||||
<span>Use this output template if non are given with URL.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -277,6 +278,11 @@ const checkInfo = async (): Promise<void> => {
|
|||
}
|
||||
}
|
||||
|
||||
if (form.folder) {
|
||||
form.folder = form.folder.trim()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
if (form.cli && '' !== form.cli) {
|
||||
const options = await convertOptions(form.cli)
|
||||
if (null === options) {
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@
|
|||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="is-bold">
|
||||
The CRON timer expression to use for this task. If not set, the task will be disabled. For more
|
||||
information on CRON expressions, see <NuxtLink to="https://crontab.guru/" target="_blank">
|
||||
The CRON timer expression to use for this task. If not set, the task runner will be disabled. For
|
||||
more information on CRON expressions, see <NuxtLink to="https://crontab.guru/" target="_blank">
|
||||
crontab.guru</NuxtLink>.
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -200,21 +200,27 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
Save in
|
||||
</label>
|
||||
<div class="control">
|
||||
<label class="label is-inline" for="folder">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
Download path
|
||||
</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control" v-tooltip="`Full Path: ${config.app.download_path}`">
|
||||
<span class="button is-static">
|
||||
<span>{{ shortPath(config.app.download_path) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input type="text" class="input" id="folder" :placeholder="getDefault('folder', '/')"
|
||||
v-model="form.folder" :disabled="addInProgress" list="folders">
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="is-bold">All folders are sub-folders of
|
||||
<code>{{ config.app.download_path }}</code>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="is-bold">
|
||||
Path relative to the download path, leave empty to use preset or default download path.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
|
|
@ -230,7 +236,9 @@
|
|||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="is-bold">The template to use for the output file name.</span>
|
||||
<span class="is-bold">
|
||||
The template to use for the output file name. Leave empty to use preset or default template.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -251,7 +259,8 @@
|
|||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="is-bold">Some URLs have special handlers to monitor for new content. Like YouTube
|
||||
channels/playlists.</span>
|
||||
channels/playlists. <span class="has-text-danger">Handlers run regardless of task timer.</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -324,6 +333,10 @@
|
|||
<Message class="is-info" :newStyle="true">
|
||||
<span>
|
||||
<ul>
|
||||
<li><strong>Tasks:</strong> requires <code>--download-archive</code> in
|
||||
<code>Command options for yt-dlp</code> can be set via presets or manually. Default presets already
|
||||
include this option.
|
||||
</li>
|
||||
<li><strong>YouTube RSS:</strong> Use <code>channel_id</code> or <code>playlist_id</code> URLs. Other link
|
||||
types (custom names, handles, user profiles) are not supported.
|
||||
</li>
|
||||
|
|
@ -334,9 +347,6 @@
|
|||
<li><strong>RSS Monitoring Basics:</strong> Runs hourly independently. Timer controls scheduled downloads to
|
||||
yt-dlp. Disable <code>Enable Handler</code> to disable RSS monitoring.
|
||||
</li>
|
||||
<li><strong>Archive Requirement:</strong> RSS monitoring requires <code>--download-archive</code> in
|
||||
<code>Command options for yt-dlp</code> (set in task or preset).
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</Message>
|
||||
|
|
@ -420,6 +430,11 @@ const checkInfo = async (): Promise<void> => {
|
|||
}
|
||||
}
|
||||
|
||||
if (form.folder) {
|
||||
form.folder = form.folder.trim()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
if (form.timer) {
|
||||
try {
|
||||
CronExpressionParser.parse(form.timer)
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
<p :class="{ 'is-text-overflow': !isExpanded(item.id, 'folder'), 'is-clickable': true }"
|
||||
v-if="item.folder" @click="toggleExpand(item.id, 'folder')"
|
||||
:title="!isExpanded(item.id, 'folder') ? 'Click to expand' : 'Click to collapse'">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>{{ calcPath(item.folder) }}</span>
|
||||
</p>
|
||||
<p :class="{ 'is-text-overflow': !isExpanded(item.id, 'template'), 'is-clickable': true }"
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@
|
|||
<span v-else class="has-text-danger">No timer or handler</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>{{ calcPath(item.folder) }}</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.template">
|
||||
|
|
|
|||
|
|
@ -703,11 +703,26 @@ const stripPath = (base_path: string, real_path: string): string => {
|
|||
|
||||
return real_path.replace(base_path, '').replace(/^\//, '')
|
||||
}
|
||||
const shortPath = (path: string, prefix: string = '...'): string => {
|
||||
if (typeof path !== 'string') {
|
||||
return path;
|
||||
}
|
||||
|
||||
const hasTrailingSlash = /\/$/.test(path);
|
||||
const clean = path.replace(/\/+$/, '');
|
||||
const parts = clean.split('/').filter(Boolean);
|
||||
|
||||
if (parts.length <= 1) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return `${prefix}/${parts.at(-1)}${hasTrailingSlash ? '/' : ''}`;
|
||||
}
|
||||
|
||||
export {
|
||||
separators, convertCliOptions, getSeparatorsName, iTrim, eTrim, sTrim, ucFirst,
|
||||
getValue, ag, ag_set, awaitElement, r, copyText, dEvent, makePagination, encodePath,
|
||||
request, removeANSIColors, dec2hex, makeId, basename, dirname, getQueryParams,
|
||||
makeDownload, formatBytes, has_data, toggleClass, cleanObject, uri, formatTime,
|
||||
sleep, awaiter, encode, decode, disableOpacity, enableOpacity, stripPath
|
||||
sleep, awaiter, encode, decode, disableOpacity, enableOpacity, stripPath, shortPath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"moment": "^2.30.1",
|
||||
"nuxt": "^4.2.2",
|
||||
"pinia": "^3.0.4",
|
||||
"socket.io-client": "^4.8.2",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-toastification": "2.0.0-rc.5"
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ importers:
|
|||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
||||
socket.io-client:
|
||||
specifier: ^4.8.2
|
||||
version: 4.8.2
|
||||
specifier: ^4.8.3
|
||||
version: 4.8.3
|
||||
vue:
|
||||
specifier: ^3.5.26
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
|
|
@ -1267,8 +1267,8 @@ packages:
|
|||
'@rolldown/pluginutils@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5':
|
||||
resolution: {integrity: sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ==}
|
||||
'@rolldown/pluginutils@1.0.0-beta.57':
|
||||
resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==}
|
||||
|
||||
'@rollup/plugin-alias@5.1.1':
|
||||
resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==}
|
||||
|
|
@ -1577,8 +1577,8 @@ packages:
|
|||
resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@unhead/vue@2.0.19':
|
||||
resolution: {integrity: sha512-7BYjHfOaoZ9+ARJkT10Q2TjnTUqDXmMpfakIAsD/hXiuff1oqWg1xeXT5+MomhNcC15HbiABpbbBmITLSHxdKg==}
|
||||
'@unhead/vue@2.1.1':
|
||||
resolution: {integrity: sha512-WYa8ORhfv7lWDSoNpkMKhbW1Dbsux/3HqMcVkZS3xZ2/c/VrcChLj+IMadpCd1WNR0srITfRJhBYZ1i9hON5Qw==}
|
||||
peerDependencies:
|
||||
vue: '>=3.5.18'
|
||||
|
||||
|
|
@ -1690,11 +1690,11 @@ packages:
|
|||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.2':
|
||||
resolution: {integrity: sha512-3a2BOryRjG/Iih87x87YXz5c8nw27eSlHytvSKYfp8ZIsp5+FgFQoKeA7k2PnqWpjJrv6AoVTMnvmuKUXb771A==}
|
||||
'@vitejs/plugin-vue-jsx@5.1.3':
|
||||
resolution: {integrity: sha512-I6Zr8cYVr5WHMW5gNOP09DNqW9rgO8RX73Wa6Czgq/0ndpTfJM4vfDChfOT1+3KtdrNqilNBtNlFwVeB02ZzGw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
|
||||
vue: ^3.0.0
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3':
|
||||
|
|
@ -1877,8 +1877,8 @@ packages:
|
|||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
alien-signals@3.1.1:
|
||||
resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==}
|
||||
alien-signals@3.1.2:
|
||||
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
|
|
@ -3831,8 +3831,8 @@ packages:
|
|||
serialize-javascript@6.0.2:
|
||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||
|
||||
seroval@1.4.1:
|
||||
resolution: {integrity: sha512-9GOc+8T6LN4aByLN75uRvMbrwY5RDBW6lSlknsY4LEa9ZmWcxKcRe1G/Q3HZXjltxMHTrStnvrwAICxZrhldtg==}
|
||||
seroval@1.4.2:
|
||||
resolution: {integrity: sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
serve-placeholder@2.0.2:
|
||||
|
|
@ -3881,8 +3881,8 @@ packages:
|
|||
smob@1.5.0:
|
||||
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
|
||||
|
||||
socket.io-client@4.8.2:
|
||||
resolution: {integrity: sha512-4MY14EMsyEPFA6lM01XIYepRdV8P6dUir2hxAlAysOYcbNAy5QNHYgIHOcQ1KYM7wTcKnKEW/ZRoIxRinWRXvA==}
|
||||
socket.io-client@4.8.3:
|
||||
resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
socket.io-parser@4.2.5:
|
||||
|
|
@ -4130,8 +4130,8 @@ packages:
|
|||
unenv@2.0.0-rc.24:
|
||||
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
|
||||
|
||||
unhead@2.0.19:
|
||||
resolution: {integrity: sha512-gEEjkV11Aj+rBnY6wnRfsFtF2RxKOLaPN4i+Gx3UhBxnszvV6ApSNZbGk7WKyy/lErQ6ekPN63qdFL7sa1leow==}
|
||||
unhead@2.1.1:
|
||||
resolution: {integrity: sha512-NOt8n2KybAOxSLfNXegAVai4SGU8bPKqWnqCzNAvnRH2i8mW+0bbFjN/L75LBgCSTiOjJSpANe5w2V34Grr7Cw==}
|
||||
|
||||
unicorn-magic@0.3.0:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
|
|
@ -4456,6 +4456,7 @@ packages:
|
|||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-mimetype@4.0.0:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
|
|
@ -5421,7 +5422,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@nuxt/devalue': 2.0.2
|
||||
'@nuxt/kit': 4.2.2(magicast@0.5.1)
|
||||
'@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3))
|
||||
'@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3))
|
||||
'@vue/shared': 3.5.26
|
||||
consola: 3.4.2
|
||||
defu: 6.1.4
|
||||
|
|
@ -5511,7 +5512,7 @@ snapshots:
|
|||
'@nuxt/kit': 4.2.2(magicast@0.5.1)
|
||||
'@rollup/plugin-replace': 6.0.3(rollup@4.54.0)
|
||||
'@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 5.1.2(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 5.1.3(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
autoprefixer: 10.4.23(postcss@8.5.6)
|
||||
consola: 3.4.2
|
||||
cssnano: 7.1.2(postcss@8.5.6)
|
||||
|
|
@ -5531,7 +5532,7 @@ snapshots:
|
|||
pkg-types: 2.3.0
|
||||
postcss: 8.5.6
|
||||
rollup-plugin-visualizer: 6.0.5(rollup@4.54.0)
|
||||
seroval: 1.4.1
|
||||
seroval: 1.4.2
|
||||
std-env: 3.10.0
|
||||
ufo: 1.6.1
|
||||
unenv: 2.0.0-rc.24
|
||||
|
|
@ -5799,7 +5800,7 @@ snapshots:
|
|||
|
||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5': {}
|
||||
'@rolldown/pluginutils@1.0.0-beta.57': {}
|
||||
|
||||
'@rollup/plugin-alias@5.1.1(rollup@4.54.0)':
|
||||
optionalDependencies:
|
||||
|
|
@ -6072,10 +6073,10 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.50.1
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@unhead/vue@2.0.19(vue@3.5.26(typescript@5.9.3))':
|
||||
'@unhead/vue@2.1.1(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
unhead: 2.0.19
|
||||
unhead: 2.1.1
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
|
|
@ -6156,12 +6157,12 @@ snapshots:
|
|||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.2(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue-jsx@5.1.3(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||
'@rolldown/pluginutils': 1.0.0-beta.9-commit.d91dfb5
|
||||
'@rolldown/pluginutils': 1.0.0-beta.57
|
||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5)
|
||||
vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
|
@ -6345,7 +6346,7 @@ snapshots:
|
|||
'@volar/language-core': 2.4.27
|
||||
'@vue/compiler-dom': 3.5.26
|
||||
'@vue/shared': 3.5.26
|
||||
alien-signals: 3.1.1
|
||||
alien-signals: 3.1.2
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
picomatch: 4.0.3
|
||||
|
|
@ -6427,7 +6428,7 @@ snapshots:
|
|||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
alien-signals@3.1.1: {}
|
||||
alien-signals@3.1.2: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
|
|
@ -7947,7 +7948,7 @@ snapshots:
|
|||
'@nuxt/schema': 4.2.2
|
||||
'@nuxt/telemetry': 2.6.6(magicast@0.5.1)
|
||||
'@nuxt/vite-builder': 4.2.2(@types/node@22.18.1)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.26)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.54.0)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.2.1(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3))
|
||||
'@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3))
|
||||
'@vue/shared': 3.5.26
|
||||
c12: 3.3.3(magicast@0.5.1)
|
||||
chokidar: 5.0.0
|
||||
|
|
@ -8628,7 +8629,7 @@ snapshots:
|
|||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
seroval@1.4.1: {}
|
||||
seroval@1.4.2: {}
|
||||
|
||||
serve-placeholder@2.0.2:
|
||||
dependencies:
|
||||
|
|
@ -8677,7 +8678,7 @@ snapshots:
|
|||
|
||||
smob@1.5.0: {}
|
||||
|
||||
socket.io-client@4.8.2:
|
||||
socket.io-client@4.8.3:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.4.3
|
||||
|
|
@ -8928,7 +8929,7 @@ snapshots:
|
|||
dependencies:
|
||||
pathe: 2.0.3
|
||||
|
||||
unhead@2.0.19:
|
||||
unhead@2.1.1:
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue