fix merge conflicts
This commit is contained in:
commit
09c8984a9d
17 changed files with 152 additions and 26 deletions
1
.github/workflows/build-pr.yml
vendored
1
.github/workflows/build-pr.yml
vendored
|
|
@ -8,6 +8,7 @@ on:
|
|||
branches:
|
||||
- master
|
||||
- dev
|
||||
- testing
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
|
|
|||
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
|
|
@ -15,6 +15,7 @@ on:
|
|||
branches:
|
||||
- dev
|
||||
- master
|
||||
- testing
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -39,3 +39,4 @@ build
|
|||
version.txt
|
||||
test_impl.py
|
||||
./eslint.config.js
|
||||
.pytest_cache
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -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.
|
||||
|
||||
It’s a personal project designed to make downloading videos from the internet more convenient. It’s 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 I’m 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 don’t 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ FRONTEND_ROUTES: list[str] = [
|
|||
"/logs/",
|
||||
"/conditions/",
|
||||
"/browser/",
|
||||
"/settings",
|
||||
"/settings/",
|
||||
"/browser/{path:.*}",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
2
ui/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
NUXT_API_URL="http://localhost:8081/api/"
|
||||
NUXT_PUBLIC_WSS=":8081/"
|
||||
|
|
@ -370,3 +370,4 @@ div.is-centered {
|
|||
padding: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue