Merge pull request #204 from arabcoders/dev
Disable yt-dlp console access by default
This commit is contained in:
commit
243d4554d7
15 changed files with 245 additions and 31 deletions
|
|
@ -77,6 +77,7 @@ Certain configuration values can be set via environment variables, using the `-e
|
||||||
| YTP_MAX_WORKERS | How many works to use for downloads | `1` |
|
| YTP_MAX_WORKERS | How many works to use for downloads | `1` |
|
||||||
| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` |
|
| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` |
|
||||||
| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` |
|
| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` |
|
||||||
|
| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` |
|
||||||
| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` |
|
| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` |
|
||||||
| YTP_CONFIG_PATH | Path to where the queue persistence files will be saved | `/config` |
|
| YTP_CONFIG_PATH | Path to where the queue persistence files will be saved | `/config` |
|
||||||
| YTP_TEMP_PATH | Path where intermediary download files will be saved | `/tmp` |
|
| YTP_TEMP_PATH | Path where intermediary download files will be saved | `/tmp` |
|
||||||
|
|
|
||||||
|
|
@ -608,8 +608,21 @@ class HttpAPI(Common):
|
||||||
if not url:
|
if not url:
|
||||||
return web.json_response(data={"error": "url param is required."}, status=web.HTTPBadRequest.status_code)
|
return web.json_response(data={"error": "url param is required."}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
preset = request.query.get("preset")
|
||||||
|
if preset:
|
||||||
|
exists = Presets.get_instance().get(preset)
|
||||||
|
if not exists:
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": f"Preset '{preset}' does not exist."}, status=web.HTTPBadRequest.status_code
|
||||||
|
)
|
||||||
|
data["preset"] = preset
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = await self.add(**self.format_item({"url": url}))
|
status = await self.add(**self.format_item(data))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@ class HttpSocket(Common):
|
||||||
|
|
||||||
@ws_event
|
@ws_event
|
||||||
async def cli_post(self, sid: str, data):
|
async def cli_post(self, sid: str, data):
|
||||||
|
if not self.config.console_enabled:
|
||||||
|
await self.emitter.error("Console is disabled.", to=sid)
|
||||||
|
return
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
await self.emitter.emit(Events.CLI_CLOSE, {"exitcode": 0}, to=sid)
|
await self.emitter.emit(Events.CLI_CLOSE, {"exitcode": 0}, to=sid)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,9 @@ class Config:
|
||||||
secret_key: str
|
secret_key: str
|
||||||
"The secret key to use for the application."
|
"The secret key to use for the application."
|
||||||
|
|
||||||
|
console_enabled: bool = False
|
||||||
|
"Enable direct access to yt-dlp console."
|
||||||
|
|
||||||
_manual_vars: tuple = (
|
_manual_vars: tuple = (
|
||||||
"temp_path",
|
"temp_path",
|
||||||
"config_path",
|
"config_path",
|
||||||
|
|
@ -183,6 +186,7 @@ class Config:
|
||||||
"pip_ignore_updates",
|
"pip_ignore_updates",
|
||||||
"basic_mode",
|
"basic_mode",
|
||||||
"file_logging",
|
"file_logging",
|
||||||
|
"console_enabled",
|
||||||
)
|
)
|
||||||
"The variables that are booleans."
|
"The variables that are booleans."
|
||||||
|
|
||||||
|
|
@ -200,6 +204,7 @@ class Config:
|
||||||
"default_preset",
|
"default_preset",
|
||||||
"instance_title",
|
"instance_title",
|
||||||
"sentry_dsn",
|
"sentry_dsn",
|
||||||
|
"console_enabled",
|
||||||
)
|
)
|
||||||
"The variables that are relevant to the frontend."
|
"The variables that are relevant to the frontend."
|
||||||
|
|
||||||
|
|
@ -353,14 +358,13 @@ class Config:
|
||||||
|
|
||||||
# save key as bytes.
|
# save key as bytes.
|
||||||
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
|
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
|
||||||
with open(key_file,"rb") as f:
|
with open(key_file, "rb") as f:
|
||||||
self.secret_key = f.read().strip()
|
self.secret_key = f.read().strip()
|
||||||
else:
|
else:
|
||||||
self.secret_key = os.urandom(32)
|
self.secret_key = os.urandom(32)
|
||||||
with open(key_file, "wb") as f:
|
with open(key_file, "wb") as f:
|
||||||
f.write(self.secret_key)
|
f.write(self.secret_key)
|
||||||
|
|
||||||
|
|
||||||
self.started = time.time()
|
self.started = time.time()
|
||||||
|
|
||||||
def _get_attributes(self) -> dict:
|
def _get_attributes(self) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -259,3 +259,7 @@ hr {
|
||||||
.is-pre {
|
.is-pre {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-text-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
|
||||||
30
ui/components/EmbedPlayer.vue
Normal file
30
ui/components/EmbedPlayer.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<h1 class="has-text-white">Not downloaded yet.</h1>
|
||||||
|
<figure class="image is-16by9">
|
||||||
|
<iframe class="has-ratio" :src="url" frameborder="0" allowfullscreen />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emitter = defineEmits(['closeModel'])
|
||||||
|
|
||||||
|
const eventFunc = e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
emitter('closeModel')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => window.addEventListener('keydown', eventFunc))
|
||||||
|
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||||
|
</script>
|
||||||
|
|
@ -103,6 +103,12 @@
|
||||||
v-if="item.extras?.thumbnail" />
|
v-if="item.extras?.thumbnail" />
|
||||||
<img v-else src="/images/placeholder.png" />
|
<img v-else src="/images/placeholder.png" />
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="isEmbedable(item.url)" @click="embed_url = getEmbedable(item.url)" class="play-overlay">
|
||||||
|
<div class="play-icon"></div>
|
||||||
|
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||||
|
v-if="item.extras?.thumbnail" />
|
||||||
|
<img v-else src="/images/placeholder.png" />
|
||||||
|
</span>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<img @load="e => pImg(e)" v-if="item.extras?.thumbnail"
|
<img @load="e => pImg(e)" v-if="item.extras?.thumbnail"
|
||||||
:src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
|
:src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
|
||||||
|
|
@ -243,6 +249,14 @@
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="closeVideo"></button>
|
<button class="modal-close is-large" aria-label="close" @click="closeVideo"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal is-active" v-if="embed_url">
|
||||||
|
<div class="modal-background" @click="embed_url = ''"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<EmbedPlayer :url="embed_url" @closeModel="embed_url = ''" />
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large" aria-label="close" @click="embed_url = ''"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -251,6 +265,7 @@ import moment from 'moment'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
|
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
|
||||||
import toast from '~/plugins/toast'
|
import toast from '~/plugins/toast'
|
||||||
|
import { isEmbedable, getEmbedable } from '~/utils/embedable'
|
||||||
|
|
||||||
const emitter = defineEmits(['getInfo'])
|
const emitter = defineEmits(['getInfo'])
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
|
@ -263,6 +278,7 @@ const showCompleted = useStorage('showCompleted', true)
|
||||||
const hideThumbnail = useStorage('hideThumbnailHistory', false)
|
const hideThumbnail = useStorage('hideThumbnailHistory', false)
|
||||||
const direction = useStorage('sortCompleted', 'desc')
|
const direction = useStorage('sortCompleted', 'desc')
|
||||||
|
|
||||||
|
const embed_url = ref('')
|
||||||
const video_item = ref(null)
|
const video_item = ref(null)
|
||||||
|
|
||||||
const playVideo = item => video_item.value = item
|
const playVideo = item => video_item.value = item
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,18 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="false === hideThumbnail" class="card-image">
|
<div v-if="false === hideThumbnail" class="card-image">
|
||||||
<figure class="image is-3by1" v-if="item.extras?.thumbnail">
|
<figure class="image is-3by1">
|
||||||
<img :alt="item.title" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
|
<span v-if="isEmbedable(item.url)" @click="() => embed_url = getEmbedable(item.url)" class="play-overlay">
|
||||||
</figure>
|
<div class="play-icon"></div>
|
||||||
<figure class="image is-3by1" v-else>
|
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||||
<img :src="'/images/placeholder.png'" />
|
v-if="item.extras?.thumbnail" />
|
||||||
|
<img v-else src="/images/placeholder.png" />
|
||||||
|
</span>
|
||||||
|
<template v-else>
|
||||||
|
<img @load="e => pImg(e)" v-if="item.extras?.thumbnail"
|
||||||
|
:src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)" />
|
||||||
|
<img v-else src="/images/placeholder.png" />
|
||||||
|
</template>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
@ -133,6 +140,14 @@
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal is-active" v-if="embed_url">
|
||||||
|
<div class="modal-background" @click="embed_url = ''"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<EmbedPlayer :url="embed_url" @closeModel="embed_url = ''" />
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large" aria-label="close" @click="embed_url = ''"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -140,6 +155,7 @@
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { ucFirst } from '~/utils/index'
|
import { ucFirst } from '~/utils/index'
|
||||||
|
import { isEmbedable, getEmbedable } from '~/utils/embedable'
|
||||||
|
|
||||||
const emitter = defineEmits(['getInfo'])
|
const emitter = defineEmits(['getInfo'])
|
||||||
const config = useConfigStore();
|
const config = useConfigStore();
|
||||||
|
|
@ -150,6 +166,7 @@ const selectedElms = ref([]);
|
||||||
const masterSelectAll = ref(false);
|
const masterSelectAll = ref(false);
|
||||||
const showQueue = useStorage('showQueue', true)
|
const showQueue = useStorage('showQueue', true)
|
||||||
const hideThumbnail = useStorage('hideThumbnailQueue', false)
|
const hideThumbnail = useStorage('hideThumbnailQueue', false)
|
||||||
|
const embed_url = ref('')
|
||||||
|
|
||||||
watch(masterSelectAll, (value) => {
|
watch(masterSelectAll, (value) => {
|
||||||
for (const key in stateStore.queue) {
|
for (const key in stateStore.queue) {
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,23 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="infoLoaded">
|
||||||
<video class="player" ref="video" :poster="thumbnail" :title="title" playsinline controls crossorigin="anonymous"
|
<video class="player" ref="video" :poster="thumbnail" :title="title" playsinline controls crossorigin="anonymous"
|
||||||
preload="auto" autoplay>
|
preload="auto" autoplay>
|
||||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||||
:type="source.type" />
|
:type="source.type" />
|
||||||
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label" :srclang="track.lang"
|
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
||||||
:src="track.file" :default="notFirefox && i === 0" />
|
:srclang="track.lang" :src="track.file" :default="notFirefox && i === 0" />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="text-align: center;" v-else>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa-solid fa-spinner fa-spin fa-4x" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
@ -25,6 +31,7 @@ import { onMounted, onUpdated, ref, onUnmounted } from 'vue'
|
||||||
import Hls from 'hls.js'
|
import Hls from 'hls.js'
|
||||||
import { makeDownload } from '~/utils/index'
|
import { makeDownload } from '~/utils/index'
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
item: {
|
||||||
|
|
@ -46,6 +53,7 @@ const isAudio = ref(false)
|
||||||
const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)
|
const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)
|
||||||
const volume = useStorage('player_volume', 1)
|
const volume = useStorage('player_volume', 1)
|
||||||
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
|
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
|
||||||
|
const infoLoaded = ref(false)
|
||||||
|
|
||||||
let hls = null;
|
let hls = null;
|
||||||
|
|
||||||
|
|
@ -56,6 +64,18 @@ const eventFunc = e => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
const req = await request(makeDownload(config, props.item, 'api/file/info'));
|
||||||
|
|
||||||
|
const response = await req.json()
|
||||||
|
|
||||||
|
if (!req.ok) {
|
||||||
|
toast.error(`Failed to fetch video info. ${response?.error}`)
|
||||||
|
emitter('closeModel')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
infoLoaded.value = true
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
video.value.volume = volume.value
|
video.value.volume = volume.value
|
||||||
|
|
||||||
|
|
@ -63,13 +83,10 @@ onMounted(async () => {
|
||||||
volume.value = video.value.volume
|
volume.value = video.value.volume
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await (await request(makeDownload(config, props.item, 'api/file/info'))).json()
|
|
||||||
|
|
||||||
if (props.item.extras?.thumbnail) {
|
if (props.item.extras?.thumbnail) {
|
||||||
thumbnail.value = '/api/thumbnail?url=' + encodePath(props.item.extras.thumbnail)
|
thumbnail.value = '/api/thumbnail?url=' + encodePath(props.item.extras.thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -- check if mimetype is video/mp4 and device is apple
|
// -- check if mimetype is video/mp4 and device is apple
|
||||||
// -- as always apple, apple like to be snowflakes.
|
// -- as always apple, apple like to be snowflakes.
|
||||||
if (isApple) {
|
if (isApple) {
|
||||||
|
|
@ -135,6 +152,10 @@ onUnmounted(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const prepareVideoPlayer = () => {
|
const prepareVideoPlayer = () => {
|
||||||
|
if (!infoLoaded.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (false === ("mediaSession" in navigator)) {
|
if (false === ("mediaSession" in navigator)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<NuxtLink class="navbar-item has-tooltip-bottom" to="/"
|
<NuxtLink class="navbar-item has-tooltip-bottom" to="/"
|
||||||
v-tooltip="socket.isConnected ? 'Connected' : 'Connecting'">
|
v-tooltip="socket.isConnected ? 'Connected' : 'Connecting'">
|
||||||
<span>
|
<span>
|
||||||
<span class="icon"> <i class="fas fa-home" /></span>
|
<span class="icon"><i class="fas fa-home" /></span>
|
||||||
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'">
|
<span class="has-text-bold" :class="`has-text-${socket.isConnected ? 'success' : 'danger'}`">
|
||||||
<b>YTPTube</b>
|
YTPTube
|
||||||
</span>
|
</span>
|
||||||
<span v-if="config?.app?.instance_title">: {{ config.app.instance_title }}</span>
|
<span v-if="config?.app?.instance_title">: {{ config.app.instance_title }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
|
|
||||||
<div class="navbar-item is-hidden-tablet">
|
<div class="navbar-item is-hidden-tablet">
|
||||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/" v-tooltip.bottom="'Downloads'">
|
<NuxtLink class="button is-dark has-tooltip-bottom" to="/" v-tooltip.bottom="'Downloads'">
|
||||||
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'" class="icon"><i
|
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'" class="icon">
|
||||||
class="fa-solid fa-home" /></span>
|
<img src="/favicon.ico" /></span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item" v-if="!config.app.basic_mode">
|
<div class="navbar-item" v-if="!config.app.basic_mode && config.app.console_enabled">
|
||||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console" v-tooltip.bottom="'Terminal'">
|
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console" v-tooltip.bottom="'Terminal'">
|
||||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,20 @@ watch(() => isLoading.value, async value => {
|
||||||
focusInput()
|
focusInput()
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(() => config.app.basic_mode, async () => {
|
||||||
|
if (!config.app.basic_mode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await navigateTo('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => config.app.console_enabled, async () => {
|
||||||
|
if (config.app.console_enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await navigateTo('/')
|
||||||
|
})
|
||||||
|
|
||||||
const reSizeTerminal = () => {
|
const reSizeTerminal = () => {
|
||||||
if (!terminal.value) {
|
if (!terminal.value) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,22 @@ div.is-centered {
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p class="is-text-overflow">
|
||||||
<span class="icon"><i class="fa-solid fa-f" /></span>
|
<span class="icon"><i class="fa-solid fa-f" /></span>
|
||||||
<span v-text="item.format" />
|
<span v-text="item.format" />
|
||||||
</p>
|
</p>
|
||||||
|
<p class="is-text-overflow" v-if="item.folder">
|
||||||
|
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||||
|
<span>{{ calcPath(item.folder) }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="is-text-overflow" v-if="item.template">
|
||||||
|
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||||
|
<span>{{ item.template }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="is-text-overflow" v-if="item.cookies">
|
||||||
|
<span class="icon"><i class="fa-solid fa-cookie" /></span>
|
||||||
|
<span>Has cookies</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content" v-if="item?.raw">
|
<div class="card-content" v-if="item?.raw">
|
||||||
|
|
@ -269,4 +281,14 @@ const copyItem = item => {
|
||||||
|
|
||||||
return copyText(JSON.stringify(data))
|
return copyText(JSON.stringify(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calcPath = path => {
|
||||||
|
const loc = config.app.download_path || '/downloads'
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
return loc + '/' + sTrim(path, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return loc
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -71,21 +71,19 @@ div.is-centered {
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p class="is-text-overflow">
|
||||||
<span class="icon"><i class="fa-solid fa-clock" /></span>
|
<span class="icon"><i class="fa-solid fa-clock" /></span>
|
||||||
<span>
|
<span>{{ tryParse(item.timer) }} - {{ item.timer }}</span>
|
||||||
{{ tryParse(item.timer) }} - {{ item.timer }}
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
<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-folder" /></span>
|
||||||
<span>{{ item.folder ? calcPath(item.folder) : config.app.download_path }}</span>
|
<span>{{ calcPath(item.folder) }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p class="is-text-overflow" v-if="item.template">
|
||||||
<span class="icon"><i class="fa-solid fa-file" /></span>
|
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||||
<span>{{ item.template ? item.template : config.app.output_template }}</span>
|
<span>{{ item.template }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p class="is-text-overflow">
|
||||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -286,7 +284,7 @@ const calcPath = path => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!socket.isConnected){
|
if (!socket.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket.on('status', statusHandler)
|
socket.on('status', statusHandler)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const CONFIG_KEYS = {
|
||||||
default_preset: 'default',
|
default_preset: 'default',
|
||||||
instance_title: null,
|
instance_title: null,
|
||||||
sentry_dsn: null,
|
sentry_dsn: null,
|
||||||
|
console_enabled: false,
|
||||||
},
|
},
|
||||||
presets: [
|
presets: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
69
ui/utils/embedable.js
Normal file
69
ui/utils/embedable.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
const sources = [
|
||||||
|
{
|
||||||
|
name: 'youtube',
|
||||||
|
url: 'https://www.youtube-nocookie.com/embed/',
|
||||||
|
regex: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instagram",
|
||||||
|
url: "https://www.instagram.com/p/",
|
||||||
|
regex: /https?:\/\/(?:www\.)?instagram\.com\/p\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "twitter",
|
||||||
|
url: "https://twitter.com/i/status/",
|
||||||
|
regex: /https?:\/\/(?:www\.)?twitter\.com\/i\/status\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "facebook",
|
||||||
|
url: "https://www.facebook.com/plugins/post.php?href=",
|
||||||
|
regex: /https?:\/\/(?:www\.)?facebook\.com\/(?:[^/?#&]+)\/posts\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tiktok",
|
||||||
|
url: "https://www.tiktok.com/embed/",
|
||||||
|
regex: /https?:\/\/(?:www\.)?tiktok\.com\/(?:[^/?#&]+)\/video\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vimeo",
|
||||||
|
url: "https://player.vimeo.com/video/",
|
||||||
|
regex: /https?:\/\/(?:www\.)?vimeo\.com\/(?:[^/?#&]+)\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "soundcloud",
|
||||||
|
url: "https://w.soundcloud.com/player/?url=",
|
||||||
|
regex: /https?:\/\/(?:www\.)?soundcloud\.com\/(?:[^/?#&]+)\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spotify",
|
||||||
|
url: "https://open.spotify.com/embed/",
|
||||||
|
regex: /https?:\/\/(?:open\.)?spotify\.com\/(?:[^/?#&]+)\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "twitch",
|
||||||
|
url: "https://player.twitch.tv/?channel=",
|
||||||
|
regex: /https?:\/\/(?:www\.)?twitch\.tv\/(?:[^/?#&]+)\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixcloud",
|
||||||
|
url: "https://www.mixcloud.com/widget/iframe/",
|
||||||
|
regex: /https?:\/\/(?:www\.)?mixcloud\.com\/(?:[^/?#&]+)\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dailymotion",
|
||||||
|
url: "https://www.dailymotion.com/embed/video/",
|
||||||
|
regex: /https?:\/\/(?:www\.)?dailymotion\.com\/(?:[^/?#&]+)\/video\/([^/?#&]+)/,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const isEmbedable = url => {
|
||||||
|
return sources.some(source => source.regex.test(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEmbedable = url => {
|
||||||
|
const source = sources.find(source => source.regex.test(url));
|
||||||
|
const id = source.regex.exec(url)[1];
|
||||||
|
return source.url + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isEmbedable, getEmbedable };
|
||||||
Loading…
Reference in a new issue