Merge pull request #361 from arabcoders/dev
Some checks failed
Build WebView wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build WebView wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build WebView wrappers / build (arm64, windows-latest) (push) Has been cancelled

Add force download switch
This commit is contained in:
Abdulmohsen 2025-08-02 16:45:06 +03:00 committed by GitHub
commit c310ae9abc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 80 additions and 35 deletions

3
.vscode/launch.json vendored
View file

@ -27,6 +27,7 @@
"program": "app/main.py", "program": "app/main.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true, "justMyCode": true,
"subProcess": true,
"env": { "env": {
"YTP_CONFIG_PATH": "${workspaceFolder}/var/config", "YTP_CONFIG_PATH": "${workspaceFolder}/var/config",
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads", "YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
@ -34,8 +35,6 @@
"PYDEVD_DISABLE_FILE_VALIDATION": "1", "PYDEVD_DISABLE_FILE_VALIDATION": "1",
"YTP_IGNORE_UI": "true" "YTP_IGNORE_UI": "true"
}, },
"subProcess": true,
"postDebugTask": "kill-debugpy"
}, },
{ {
"name": "Node: Generate UI", "name": "Node: Generate UI",

22
.vscode/tasks.json vendored
View file

@ -1,22 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "kill-debugpy",
"type": "shell",
"command": "pkill -f debugpy || true",
"problemMatcher": [],
"presentation": {
"echo": false,
"reveal": "never",
"focus": false,
"panel": "dedicated",
"showReuseMessage": false,
"clear": false
},
"runOptions": {
"runOn": "folderOpen"
}
}
]
}

View file

@ -317,4 +317,5 @@ Certain configuration values can be set via environment variables, using the `-e
| YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer | `15 */1 * * *` | | YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer | `15 */1 * * *` |
| YTP_PLAYLIST_ITEMS_CONCURRENCY | The number of playlist items be to processed at same time | `1` | | YTP_PLAYLIST_ITEMS_CONCURRENCY | The number of playlist items be to processed at same time | `1` |
| YTP_TEMP_DISABLED | Disable temp files handling. | `false` | | YTP_TEMP_DISABLED | Disable temp files handling. | `false` |
| YTP_DOWNLOAD_PATH_DEPTH | How many subdirectories to show in auto complete. | `1` |

View file

@ -1405,6 +1405,32 @@ def find_unpickleable(obj, name="root", seen=None):
find_unpickleable(value, f"{name}.{attr}", seen) find_unpickleable(value, f"{name}.{attr}", seen)
except Exception as ie: except Exception as ie:
LOG.error(f"[ERROR] Accessing {name}.{attr}: {ie}") LOG.error(f"[ERROR] Accessing {name}.{attr}: {ie}")
elif isinstance(obj, (list, tuple, set)): elif isinstance(obj, list | tuple | set):
for idx, item in enumerate(obj): for idx, item in enumerate(obj):
find_unpickleable(item, f"{name}[{idx}]", seen) find_unpickleable(item, f"{name}[{idx}]", seen)
def list_folders(path: Path, base: Path, depth_limit: int) -> list[str]:
"""
List all folders relative to a base path, up to a specified depth limit.
Args:
path (Path): The path to start listing folders from.
base (Path): The base path to which the folders should be relative.
depth_limit (int): The maximum depth to traverse from the base path.
Returns:
list[str]: A list of folder paths relative to the base path, up to the specified
"""
rel_depth: int = len(path.relative_to(base).parts)
if rel_depth > depth_limit:
return []
folders: list[str] = []
for entry in path.iterdir():
if entry.is_dir():
folders.append(str(entry.relative_to(base)))
folders.extend(list_folders(entry, base, depth_limit))
return folders

View file

@ -25,6 +25,9 @@ class Config:
download_path: str = "." download_path: str = "."
"""The path to the download directory.""" """The path to the download directory."""
download_path_depth: int = 2
"""How many subdirectories to show in auto complete."""
temp_path: str = "/tmp" temp_path: str = "/tmp"
"""The path to the temporary directory.""" """The path to the temporary directory."""
@ -219,6 +222,7 @@ class Config:
"extract_info_timeout", "extract_info_timeout",
"debugpy_port", "debugpy_port",
"playlist_items_concurrency", "playlist_items_concurrency",
"download_path_depth",
) )
"The variables that are integers." "The variables that are integers."

View file

@ -10,7 +10,7 @@ from app.library.DownloadQueue import DownloadQueue
from app.library.Events import EventBus, Events from app.library.Events import EventBus, Events
from app.library.Presets import Presets from app.library.Presets import Presets
from app.library.router import RouteType, route from app.library.router import RouteType, route
from app.library.Utils import tail_log from app.library.Utils import list_folders, tail_log
LOG: logging.Logger = logging.getLogger(__name__) LOG: logging.Logger = logging.getLogger(__name__)
@ -29,7 +29,11 @@ async def connect(config: Config, queue: DownloadQueue, notify: EventBus, sid: s
"paused": queue.is_paused(), "paused": queue.is_paused(),
} }
data["folders"] = [folder.name for folder in Path(config.download_path).iterdir() if folder.is_dir()] data["folders"] = list_folders(
path=Path(config.download_path),
base=Path(config.download_path),
depth_limit=config.download_path_depth-1,
)
await notify.emit( await notify.emit(
Events.CONNECTED, Events.CONNECTED,

View file

@ -88,21 +88,31 @@
</div> </div>
<div class="columns is-multiline is-mobile" v-if="showAdvanced && !config.app.basic_mode"> <div class="columns is-multiline is-mobile" v-if="showAdvanced && !config.app.basic_mode">
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode"> <div class="column is-3-tablet is-12-mobile" v-if="!config.app.basic_mode">
<div class="field"> <div class="field">
<input id="auto_start" type="checkbox" v-model="auto_start" :disabled="addInProgress" <input id="force_download" type="checkbox" class="switch is-danger" :checked="forceDownload"
class="switch is-success" /> @change="forceDownload = !forceDownload" :disabled="addInProgress" />
<label for="auto_start" class="is-unselectable"> <label for="force_download" class="is-unselectable">Force download</label>
{{ auto_start ? 'Auto start' : 'Manual start' }}
</label>
</div> </div>
<span class="help"> <span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">Whether to start the download automatically or wait.</span> <span class="is-bold">Ignore archive and re-download.</span>
</span> </span>
</div> </div>
<div class="column is-8-tablet is-12-mobile"> <div class="column is-3-tablet is-12-mobile" v-if="!config.app.basic_mode">
<div class="field">
<input id="auto_start" type="checkbox" v-model="auto_start" :disabled="addInProgress"
class="switch is-success" />
<label for="auto_start" class="is-unselectable">Auto start</label>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">Whether to start the download automatically.</span>
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<label for="output_format" class="button is-static is-unselectable"> <label for="output_format" class="button is-static is-unselectable">
@ -268,6 +278,7 @@ const dialog_confirm = ref({
message: '', message: '',
options: [], options: [],
}) })
const FORCE_FLAG = '--no-download-archive'
const addDownload = async () => { const addDownload = async () => {
if (form.value?.cli && '' !== form.value.cli) { if (form.value?.cli && '' !== form.value.cli) {
@ -454,4 +465,26 @@ const get_output_template = () => {
return config.app.output_template || '%(title)s.%(ext)s' return config.app.output_template || '%(title)s.%(ext)s'
} }
const forceDownload = computed({
get(): boolean {
return new RegExp(`(^|\\s)${FORCE_FLAG}(\\s|$)`).test(form.value.cli || '')
},
set(val: boolean): void {
const cli = form.value.cli || ''
if (val) {
if (!cli.includes(FORCE_FLAG)) {
form.value.cli = cli.trim() + (cli.trim() ? ` ${FORCE_FLAG}` : FORCE_FLAG)
}
} else {
form.value.cli = cli
.replace(new RegExp(`(\\s*)${FORCE_FLAG}(\\s*)`, 'g'), (match, before, after) => {
return before && after ? ' ' : ''
})
.replace(/[ \t]+/g, ' ')
.replace(/^[ \t]+|[ \t]+$/g, '')
.trim()
}
},
})
</script> </script>