fix merge conflicts

This commit is contained in:
arabcoders 2025-10-19 19:44:12 +03:00
commit 09c8984a9d
17 changed files with 152 additions and 26 deletions

View file

@ -8,6 +8,7 @@ on:
branches:
- master
- dev
- testing
paths-ignore:
- "**.md"
- ".github/ISSUE_TEMPLATE/**"

View file

@ -15,6 +15,7 @@ on:
branches:
- dev
- master
- testing
tags:
- "v*"
paths-ignore:

1
.gitignore vendored
View file

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

View file

@ -223,13 +223,17 @@ cd ui
pnpm install
```
> [!NOTE]
> Create a `.env` file in the `ui/` directory based on `.env.example` to configure environment variables.
> to link the frontend to the backend API.
3. **Run the frontend development server:**
```bash
pnpm dev
```
The frontend will be available at `http://localhost:3000` (or the port shown in the terminal)
The frontend will be available at `http://localhost:8082` (or the port shown in the terminal)
4. **Verify the setup:**

View file

@ -99,8 +99,17 @@ For simple API documentation, you can refer to the [API documentation](API.md).
# Disclaimer
This project is not affiliated with yt-dlp, or any other service. It's a personal project that was created to
make downloading videos from the internet easier. It's not intended to be used for piracy or any other illegal activities.
This project is not affiliated with yt-dlp or any other service.
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

View file

@ -438,7 +438,7 @@ class Notification(metaclass=Singleton):
import apprise
try:
notify = apprise.Apprise()
notify = apprise.Apprise(debug=self._debug)
apr_config = Path(Config.get_instance().apprise_config)
if apr_config.exists():
apprise_config = notify.AppriseConfig()
@ -448,11 +448,14 @@ class Notification(metaclass=Singleton):
for t in target:
notify.add(t.request.url)
notify.notify(
status = await notify.async_notify(
body=ev.message or json.dumps(ev.serialize(), sort_keys=False, ensure_ascii=False),
title=ev.title or f"YTPTube Event: {ev.event}",
notify_type=ev.event,
)
if not status:
msg = "Apprise failed to send notification."
raise RuntimeError(msg) # noqa: TRY301
except Exception as e:
LOG.exception(e)
LOG.error(f"Error sending Apprise notification: {e!s}")

View file

@ -30,7 +30,7 @@ FRONTEND_ROUTES: list[str] = [
"/logs/",
"/conditions/",
"/browser/",
"/settings",
"/settings/",
"/browser/{path:.*}",
]

View file

@ -812,6 +812,8 @@ class TestNotification:
# Mock apprise import
mock_apprise = Mock()
mock_notify = Mock()
# Mock async_notify as an AsyncMock that returns True
mock_notify.async_notify = AsyncMock(return_value=True)
mock_apprise.Apprise.return_value = mock_notify
mock_import.return_value = mock_apprise

2
ui/.env.example Normal file
View file

@ -0,0 +1,2 @@
NUXT_API_URL="http://localhost:8081/api/"
NUXT_PUBLIC_WSS=":8081/"

View file

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

View file

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

View file

@ -266,7 +266,7 @@
</div>
</header>
<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">
<div class="play-icon"></div>
<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 bg_enable = useStorage<boolean>('random_bg', true)
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 masterSelectAll = ref(false)

View file

@ -198,7 +198,7 @@
</div>
</header>
<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"
class="play-overlay">
<div class="play-icon embed-icon"></div>
@ -336,6 +336,7 @@ const hideThumbnail = useStorage('hideThumbnailQueue', false)
const display_style = useStorage('display_style', 'grid')
const bg_enable = useStorage('random_bg', true)
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 masterSelectAll = ref(false)

View file

@ -6,13 +6,14 @@
<label class="label" for="download-url">
What you would like to download?
<span class="icon is-pointer" :class="connectionStatusColor" @click="$emit('show_settings')"
v-tooltip="'WebUI Settings'">
<i class="fas fa-cogs" /></span>
v-tooltip="'WebUI Settings'">
<i class="fas fa-cogs" /></span>
</label>
<div class="field has-addons">
<div class="control">
<button type="button" class="button is-info" @click="() => showPresets = !showPresets">
<span class="icon"><i class="fas" :class="showPresets ? 'fa-chevron-up' : 'fa-chevron-down'" /></span>
<button type="button" class="button is-info" @click="() => showExtras = !showExtras"
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>
</div>
<div class="control is-expanded">
@ -27,7 +28,7 @@
</button>
</div>
</div>
<div class="field has-addons" v-if="showPresets">
<div class="field has-addons" v-if="showExtras">
<div class="control">
<label class="button is-static">
<span class="icon"><i class="fas fa-sliders" /></span>
@ -53,6 +54,20 @@
</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>
</section>
</div>
@ -211,7 +226,8 @@ const videoItem = ref<StoreItem | null>(null)
const formUrl = ref<string>('')
const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' })
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 sortByNewest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
@ -245,9 +261,50 @@ const addDownload = async (): Promise<void> => {
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[] = [{
url,
preset: formPreset.value.preset || app.value.default_preset,
cli: cli || '',
auto_start: true,
}]
@ -272,6 +329,7 @@ const addDownload = async (): Promise<void> => {
formUrl.value = ''
formPreset.value.preset = app.value.default_preset || ''
dlFields.value = {}
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to add download.'
toast.error(message)
@ -542,6 +600,9 @@ const showMessage = (item: StoreItem) => {
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(() => {
switch (socketStore.connectionStatus) {
case 'connected':

View file

@ -231,6 +231,28 @@
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="!reference">
<div class="field">
<label class="label is-inline" for="archive_all_after_add">
<span class="icon"><i class="fa-solid fa-box-archive" /></span>
Mark all existing items as downloaded
</label>
<div class="control is-unselectable">
<input id="archive_all_after_add" type="checkbox" v-model="archiveAllAfterAdd" :disabled="addInProgress"
class="switch is-danger" />
<label for="archive_all_after_add" class="is-unselectable">
{{ archiveAllAfterAdd ? 'Yes' : 'No' }}
</label>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">
If enabled, all existing items in the feed will be marked as downloaded after adding the task.
</span>
</span>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label is-unselectable" for="cli_options">
@ -317,7 +339,7 @@ const props = defineProps<{
const emitter = defineEmits<{
(e: 'cancel'): void
(e: 'submit', payload: { reference: string | null | undefined, task: task_item }): void
(e: 'submit', payload: { reference: string | null | undefined, task: task_item, archive_all?: boolean }): void
}>()
const toast = useNotification()
@ -329,6 +351,7 @@ const convertInProgress = ref<boolean>(false)
const import_string = ref<string>('')
const showOptions = ref<boolean>(false)
const ytDlpOpt = ref<AutoCompleteOptions>([])
const archiveAllAfterAdd = ref<boolean>(false)
const CHANNEL_REGEX = /^https?:\/\/(?:www\.)?youtube\.com\/(?:(?:channel\/(?<channelId>UC[0-9A-Za-z_-]{22}))|(?:c\/(?<customName>[A-Za-z0-9_-]+))|(?:user\/(?<userName>[A-Za-z0-9_-]+))|(?:@(?<handle>[A-Za-z0-9_-]+)))(?<suffix>\/.*)?\/?$/
@ -389,7 +412,7 @@ const checkInfo = async (): Promise<void> => {
form.cli = form.cli.trim()
}
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) })
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form), archive_all: archiveAllAfterAdd.value })
}
const importItem = async (): Promise<void> => {

View file

@ -19,7 +19,7 @@
</header>
<div class="card-content">
<div class="columns is-multiline">
<div class="column is-6">
<div class="column is-12">
<settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
</div>
</div>

View file

@ -422,6 +422,7 @@ import Modal from '~/components/Modal.vue'
import { useConfirm } from '~/composables/useConfirm'
import TaskInspect from '~/components/TaskInspect.vue'
import type { task_item, exported_task, error_response } from '~/types/tasks'
import { sleep } from '~/utils'
const box = useConfirm()
const toast = useNotification()
@ -634,7 +635,7 @@ const deleteItem = async (item: task_item) => {
toast.success('Task deleted.')
}
const updateItem = async ({ reference, task }: { reference?: string | null | undefined, task: task_item }) => {
const updateItem = async ({ reference, task, archive_all }: { reference?: string | null | undefined, task: task_item, archive_all?: boolean }) => {
if (reference) {
// -- find the task index.
const index = tasks.value.findIndex((t) => t?.id === reference)
@ -651,6 +652,16 @@ const updateItem = async ({ reference, task }: { reference?: string | null | und
}
toast.success('Task updated.')
if (!reference && true === archive_all) {
const newTask = tasks.value[tasks.value.length - 1]
if (newTask) {
await sleep(1)
await nextTick()
await archiveAll(newTask, true)
}
}
resetForm(true)
}
@ -817,14 +828,17 @@ const get_tags = (name: string): Array<string> => {
const remove_tags = (name: string): string => name.replace(/\[(.*?)\]/g, '').trim();
const archiveAll = async (item: task_item) => {
const archiveAll = async (item: task_item, by_pass: boolean = false) => {
try {
const { status } = await cDialog({
message: `Mark all '${item.name}' items as downloaded in download archive?`
})
if (true !== status) {
return;
if (true !== by_pass) {
const { status } = await cDialog({
message: `Mark all '${item.name}' items as downloaded in download archive?`
})
if (true !== status) {
return;
}
}
item.in_progress = true