Merge pull request #451 from arabcoders/dev

[FEAT] add custom download fields to simple mode.
This commit is contained in:
Abdulmohsen 2025-10-16 17:23:02 +03:00 committed by GitHub
commit 618ce4d387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 101 additions and 9 deletions

1
.gitignore vendored
View file

@ -39,3 +39,4 @@ build
version.txt version.txt
test_impl.py test_impl.py
./eslint.config.js ./eslint.config.js
.pytest_cache

View file

@ -99,8 +99,17 @@ For simple API documentation, you can refer to the [API documentation](API.md).
# Disclaimer # Disclaimer
This project is not affiliated with yt-dlp, or any other service. It's a personal project that was created to This project is not affiliated with yt-dlp or any other service.
make downloading videos from the internet easier. It's not intended to be used for piracy or any other illegal activities.
Its a personal project designed to make downloading videos from the internet more convenient. Its not intended for
piracy or any unlawful use.
This project was built primarily for my own needs and preferences. The UI might not be the most polished or visually
refined, but Im happy with it as it is. You can, however, create and load your own UI for complete customization. I
plan to refactor the UI/UX in the future using [Nuxt/ui](https://ui.nuxt.com/).
Contributions are welcome, but I may decline changes that dont align with my vision for the project. Unsolicited pull
requests may be ignored. For suggestions or feature requests, please open a discussion or join the Discord server.
# Social contact # Social contact

View file

@ -370,3 +370,4 @@ div.is-centered {
padding: 0; padding: 0;
margin: 0 auto; margin: 0 auto;
} }

View file

@ -8,7 +8,7 @@
<div class="is-block" style="word-break: all;" v-if="props.title"> <div class="is-block" style="word-break: all;" v-if="props.title">
<span style="font-size: 120%;">{{ props.title }}</span> <span style="font-size: 120%;">{{ props.title }}</span>
</div> </div>
<figure class="image is-3by1 is-hidden-mobile"> <figure :class="['image', thumbnail_ratio, 'is-hidden-mobile']">
<img @load="e => pImg(e)" :src="url" :alt="props.title" @error="clearCache" <img @load="e => pImg(e)" :src="url" :alt="props.title" @error="clearCache"
:crossorigin="props.privacy ? 'anonymous' : 'use-credentials'" :crossorigin="props.privacy ? 'anonymous' : 'use-credentials'"
:referrerpolicy="props.privacy ? 'no-referrer' : 'origin'" /> :referrerpolicy="props.privacy ? 'no-referrer' : 'origin'" />
@ -20,6 +20,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { disableOpacity, enableOpacity } from '~/utils' import { disableOpacity, enableOpacity } from '~/utils'
const props = defineProps<{ const props = defineProps<{
@ -31,6 +32,7 @@ const props = defineProps<{
const cache = useSessionCache() const cache = useSessionCache()
const toast = useNotification() const toast = useNotification()
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1')
const url = ref<string | null>(null) const url = ref<string | null>(null)
const error = ref(false) const error = ref(false)
const isPreloading = ref(false) const isPreloading = ref(false)

View file

@ -266,7 +266,7 @@
</div> </div>
</header> </header>
<div v-if="showThumbnails" class="card-image"> <div v-if="showThumbnails" class="card-image">
<figure class="image is-3by1"> <figure :class="['image', thumbnail_ratio]">
<span v-if="'finished' === item.status && item.filename" @click="playVideo(item)" class="play-overlay"> <span v-if="'finished' === item.status && item.filename" @click="playVideo(item)" class="play-overlay">
<div class="play-icon"></div> <div class="play-icon"></div>
<img @load="(e: Event) => pImg(e)" :src="getImage(item)" v-if="getImage(item)" /> <img @load="(e: Event) => pImg(e)" :src="getImage(item)" v-if="getImage(item)" />
@ -475,6 +475,7 @@ const direction = useStorage<'asc' | 'desc'>('sortCompleted', 'desc')
const display_style = useStorage<'grid' | 'list'>('display_style', 'grid') const display_style = useStorage<'grid' | 'list'>('display_style', 'grid')
const bg_enable = useStorage<boolean>('random_bg', true) const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95) const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1')
const selectedElms = ref<string[]>([]) const selectedElms = ref<string[]>([])
const masterSelectAll = ref(false) const masterSelectAll = ref(false)

View file

@ -198,7 +198,7 @@
</div> </div>
</header> </header>
<div v-if="showThumbnails" class="card-image"> <div v-if="showThumbnails" class="card-image">
<figure class="image is-3by1"> <figure :class="['image', thumbnail_ratio]">
<span v-if="isEmbedable(item.url)" @click="embed_url = getEmbedable(item.url) as string" <span v-if="isEmbedable(item.url)" @click="embed_url = getEmbedable(item.url) as string"
class="play-overlay"> class="play-overlay">
<div class="play-icon embed-icon"></div> <div class="play-icon embed-icon"></div>
@ -336,6 +336,7 @@ const hideThumbnail = useStorage('hideThumbnailQueue', false)
const display_style = useStorage('display_style', 'grid') const display_style = useStorage('display_style', 'grid')
const bg_enable = useStorage('random_bg', true) const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.95) const bg_opacity = useStorage('random_bg_opacity', 0.95)
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1')
const selectedElms = ref<string[]>([]) const selectedElms = ref<string[]>([])
const masterSelectAll = ref(false) const masterSelectAll = ref(false)

View file

@ -104,6 +104,21 @@
<p class="help is-bold"> Show videos thumbnail if available.</p> <p class="help is-bold"> Show videos thumbnail if available.</p>
</div> </div>
<div class="field" v-if="show_thumbnail">
<label class="label is-unselectable">Thumbnail Aspect Ratio</label>
<div class="control">
<label for="ratio_16by9" class="radio">
<input id="ratio_16by9" type="radio" v-model="thumbnail_ratio" value="is-16by9">
<span>&nbsp;16:9</span>
</label>
<label for="ratio_3by1" class="radio">
<input id="ratio_3by1" type="radio" v-model="thumbnail_ratio" value="is-3by1">
<span>&nbsp;3:1</span>
</label>
</div>
<p class="help is-bold">Choose the aspect ratio for thumbnail display.</p>
</div>
<div class="field"> <div class="field">
<label class="label" for="allow_toasts">Show notifications</label> <label class="label" for="allow_toasts">Show notifications</label>
<div class="control"> <div class="control">
@ -164,6 +179,7 @@ const allow_toasts = useStorage<boolean>('allow_toasts', true)
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT) const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true) const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
const show_thumbnail = useStorage<boolean>('show_thumbnail', true) const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'is-3by1')
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',') const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
const simpleMode = useStorage<boolean>('simple_mode', useConfigStore().app.simple_mode || false) const simpleMode = useStorage<boolean>('simple_mode', useConfigStore().app.simple_mode || false)
</script> </script>

View file

@ -13,8 +13,9 @@
</label> </label>
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<button type="button" class="button is-info" @click="() => showPresets = !showPresets"> <button type="button" class="button is-info" @click="() => showExtras = !showExtras"
<span class="icon"><i class="fas" :class="showPresets ? 'fa-chevron-up' : 'fa-chevron-down'" /></span> v-tooltip="showExtras ? 'Hide extra options' : 'Show extra options'">
<span class="icon"><i class="fas" :class="showExtras ? 'fa-chevron-up' : 'fa-chevron-down'" /></span>
</button> </button>
</div> </div>
<div class="control is-expanded"> <div class="control is-expanded">
@ -29,7 +30,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="field has-addons" v-if="showPresets"> <div class="field has-addons" v-if="showExtras">
<div class="control"> <div class="control">
<label class="button is-static"> <label class="button is-static">
<span class="icon"><i class="fas fa-sliders" /></span> <span class="icon"><i class="fas fa-sliders" /></span>
@ -55,6 +56,20 @@
</div> </div>
</div> </div>
<div class="columns is-multiline is-mobile" v-if="showExtras && configStore.dl_fields.length > 0">
<div class="column is-6-tablet is-12-mobile">
<DLInput id="force_download" type="bool" label="Force download"
v-model="dlFields['--no-download-archive']" icon="fa-solid fa-download"
:disabled="!socketStore.isConnected || addInProgress" description="Ignore archive and re-download." />
</div>
<div class="column is-6-tablet is-12-mobile" v-for="(fi, index) in sortedDLFields"
:key="fi.id || `dlf-${index}`">
<DLInput :id="fi?.id || `dlf-${index}`" :type="fi.kind" :description="fi.description" :label="fi.name"
:icon="fi.icon" v-model="dlFields[fi.field]" :field="fi.field"
:disabled="!socketStore.isConnected || addInProgress" />
</div>
</div>
</form> </form>
</section> </section>
</div> </div>
@ -213,7 +228,8 @@ const videoItem = ref<StoreItem | null>(null)
const formUrl = ref<string>('') const formUrl = ref<string>('')
const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' }) const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' })
const addInProgress = ref<boolean>(false) const addInProgress = ref<boolean>(false)
const showPresets = ref<boolean>(false) const showExtras = ref<boolean>(false)
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
const show_thumbnail = useStorage<boolean>('show_thumbnail', true) const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const sortByNewest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)) const sortByNewest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
@ -247,9 +263,50 @@ const addDownload = async (): Promise<void> => {
return return
} }
let cli = ''
const dlFieldsExtra = ['--no-download-archive']
const is_valid = (dl_field: string): boolean => {
if (dlFieldsExtra.includes(dl_field)) {
return true
}
if (configStore.dl_fields && configStore.dl_fields.length > 0) {
return configStore.dl_fields.some(f => dl_field === f.field)
}
return false
}
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
const joined = []
for (const [key, value] of Object.entries(dlFields.value)) {
if (false === is_valid(key)) {
continue
}
if ([undefined, null, '', false].includes(value as any)) {
continue
}
const keyRegex = new RegExp(`(^|\\s)${key}(\\s|$)`)
if (cli && keyRegex.test(cli)) {
continue
}
joined.push(true === value ? `${key}` : `${key} ${value}`)
}
if (joined.length > 0) {
cli = joined.join(' ')
}
}
const payload: item_request[] = [{ const payload: item_request[] = [{
url, url,
preset: formPreset.value.preset || app.value.default_preset, preset: formPreset.value.preset || app.value.default_preset,
cli: cli || '',
auto_start: true, auto_start: true,
}] }]
@ -274,6 +331,7 @@ const addDownload = async (): Promise<void> => {
formUrl.value = '' formUrl.value = ''
formPreset.value.preset = app.value.default_preset || '' formPreset.value.preset = app.value.default_preset || ''
dlFields.value = {}
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to add download.' const message = error instanceof Error ? error.message : 'Failed to add download.'
toast.error(message) toast.error(message)
@ -544,6 +602,9 @@ const showMessage = (item: StoreItem) => {
return (item.msg?.length || 0) > 0 return (item.msg?.length || 0) > 0
} }
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
const sortedDLFields = computed(() => configStore.dl_fields.sort((a, b) => (a.order || 0) - (b.order || 0)))
const connectionStatusColor = computed(() => { const connectionStatusColor = computed(() => {
switch (socketStore.connectionStatus) { switch (socketStore.connectionStatus) {
case 'connected': case 'connected':