Merge pull request #350 from arabcoders/dev

Fixed in browser match_str to closely match yt-dlp match_str
This commit is contained in:
Abdulmohsen 2025-07-24 19:39:01 +03:00 committed by GitHub
commit fd75d3cb8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 235 additions and 231 deletions

View file

@ -207,8 +207,6 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { decode } from '~/utils/importer'
import { match_str } from '~/utils/ytdlp'
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
const emitter = defineEmits<{
@ -292,7 +290,7 @@ const run_test = async (): Promise<void> => {
}
test_data.value.in_progress = true
test_data.value.data.status = null
test_data.value.data.status = false
try {
const response = await request('/api/conditions/test', {
@ -363,7 +361,7 @@ const show_data = (): string => {
}
const logic_test = computed(() => {
if (!test_data.value.data || !test_data.value.data.data) {
if (Object.keys(test_data.value.data?.data ?? {}).length < 1) {
return null
}

View file

@ -20,8 +20,6 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const emitter = defineEmits(['open_state'])
defineProps({
label: {

View file

@ -20,19 +20,16 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { request } from '~/utils/index'
const props = defineProps < {
const props = defineProps<{
image?: string
title?: string
loader?: () => Promise < void>
privacy ?: boolean
} > ()
loader?: () => Promise<void>
privacy?: boolean
}>()
const cache = useSessionCache()
const toast = useNotification()
const url = ref < string | null > (null)
const url = ref<string | null>(null)
const error = ref(false)
const isPreloading = ref(false)

View file

@ -38,9 +38,6 @@ code {
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { request } from '~/utils/index'
const toast = useNotification()
const emitter = defineEmits<{ (e: 'closeModel'): void }>()

View file

@ -461,12 +461,8 @@
</template>
<script setup>
import { NuxtLink } from '#components'
import { useStorage } from '@vueuse/core'
import moment from 'moment'
import { getEmbedable, isEmbedable } from '~/utils/embedable'
import { formatBytes, makeDownload, uri } from '~/utils/index'
import Dropdown from './Dropdown.vue'
const emitter = defineEmits(['getInfo', 'add_new', 'getItemInfo', 'clear_search'])

View file

@ -19,8 +19,6 @@ img {
</template>
<script setup lang="ts">
import { request } from '~/utils/index'
const toast = useNotification()
const emitter = defineEmits(['closeModel'])

View file

@ -5,7 +5,6 @@
</template>
<script setup lang="ts">
import { ref, nextTick, onBeforeUnmount } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
const props = defineProps<{

View file

@ -232,7 +232,6 @@
import 'assets/css/bulma-switch.css'
import { useStorage } from '@vueuse/core'
import type { item_request } from '~/types/item'
import { getSeparatorsName, separators } from '~/utils/index'
const props = defineProps<{ item?: Partial<item_request> }>()
const emitter = defineEmits<{

View file

@ -269,7 +269,6 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { decode } from '~/utils/importer'
const emitter = defineEmits(['cancel', 'submit']);
const toast = useNotification();

View file

@ -232,7 +232,6 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { decode } from '~/utils/importer'
const emitter = defineEmits(['cancel', 'submit']);

View file

@ -315,8 +315,6 @@
<script setup>
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { ucFirst } from '~/utils/index'
import { isEmbedable, getEmbedable } from '~/utils/embedable'
const emitter = defineEmits(['getInfo', 'clear_search', 'getItemInfo'])
const props = defineProps({

View file

@ -142,7 +142,6 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { POSITION } from 'vue-toastification'
import { separators } from '~/utils/index'
defineProps<{ isLoading: boolean }>()

View file

@ -292,7 +292,6 @@
import 'assets/css/bulma-switch.css'
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import { decode } from '~/utils/importer'
const props = defineProps({
reference: {

View file

@ -28,7 +28,6 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import Hls from 'hls.js'
import { makeDownload } from '~/utils/index'
type video_track_element = {
file: string,

View file

@ -161,6 +161,9 @@ const applyPreferredColorScheme = (scheme: string) => {
for (let s = 0; s < document.styleSheets.length; s++) {
const styleSheet = document.styleSheets[s]
if (!styleSheet?.cssRules) {
continue
}
let rules: CSSRuleList
try {

View file

@ -193,7 +193,6 @@
</template>
<script setup>
import { request } from '~/utils/index'
import moment from 'moment'
import { useStorage } from '@vueuse/core'

View file

@ -115,8 +115,6 @@
</template>
<script setup lang="ts">
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
const toast = useNotification()

View file

@ -85,7 +85,6 @@ import type { item_request } from '~/types/item'
const config = useConfigStore()
const stateStore = useStateStore()
const socket = useSocketStore()
const box = useConfirm()
const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
const display_style = useStorage<string>('display_style', 'cards')

View file

@ -111,8 +111,6 @@ code {
<script setup lang="ts">
import moment from 'moment'
import { request } from '~/utils/index'
import { ref, onMounted, nextTick } from 'vue'
import { useStorage } from '@vueuse/core'
import type { log_line } from '~/types/logs'

View file

@ -140,9 +140,6 @@ div.is-centered {
</template>
<script setup>
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
const toast = useNotification()
const config = useConfigStore()
const socket = useSocketStore()

View file

@ -212,8 +212,6 @@ div.is-centered {
<script setup>
import { useStorage } from '@vueuse/core'
import { request } from '~/utils/index'
import { encode } from '~/utils/importer'
const toast = useNotification()
const config = useConfigStore()

View file

@ -328,8 +328,6 @@ div.is-centered {
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import { request, sleep } from '~/utils/index'
import { encode } from '~/utils/importer'
import type { task_item, exported_task, error_response } from '~/types/tasks'
const box = useConfirm()
@ -377,14 +375,14 @@ watch(query, () => {
selectedElms.value = []
})
watch(masterSelectAll, (value) => {
watch(masterSelectAll, value => {
if (!value) {
selectedElms.value = []
return
}
for (const key in filteredTasks.value) {
const element = filteredTasks.value[key]
if (value) {
selectedElms.value.push(element.id)
} else {
selectedElms.value = []
}
const element = filteredTasks.value[key] as task_item
selectedElms.value.push(element.id)
}
})

View file

@ -0,0 +1,38 @@
let observer: MutationObserver;
const AUTO_SCROLL_EVENTS = ['scroll', 'mousewheel', 'touchmove'] as const;
const events_handlers: Array<(ev: Event) => void> = [];
const on_mounted = (el: HTMLElement) => {
let scrolledToBottom = true;
AUTO_SCROLL_EVENTS.forEach((ev, index) => {
const handler = () => {
scrolledToBottom = (el.scrollHeight - el.scrollTop) - el.clientHeight <= 80;
};
events_handlers[index] = handler;
el.addEventListener(ev as string, handler, { passive: true } as AddEventListenerOptions);
});
observer = new MutationObserver(() => {
if (false === scrolledToBottom) return;
el.scrollTop = el.scrollHeight;
});
observer.observe(el, { childList: true, subtree: true });
}
const on_unmounted = (el: HTMLElement) => {
AUTO_SCROLL_EVENTS.forEach((ev, index) => {
const handler = events_handlers[index];
if (handler) {
el.removeEventListener(ev as string, handler, { passive: true } as EventListenerOptions);
}
});
observer.disconnect();
}
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.directive('autoscroll', {
mounted: on_mounted,
unmounted: on_unmounted,
});
});

54
ui/app/plugins/rtime.ts Normal file
View file

@ -0,0 +1,54 @@
import moment from 'moment'
type RTimeElement = HTMLElement & { _next_timer?: number }
const parseInterval = (arg: string | undefined): number => {
if (!arg) {
return 60 * 1000
}
const match = arg.match(/^(\d+)([smhd])$/)
if (!match) {
return 60 * 1000
}
const [, numStr, unit] = match
const num = parseInt(String(numStr), 10)
switch (unit) {
case 'd': return num * 24 * 3600 * 1000
case 'h': return num * 3600 * 1000
case 'm': return num * 60 * 1000
case 's': return num * 1000
default: return 60 * 1000
}
}
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.directive('rtime', {
mounted(el: RTimeElement, binding) {
const intervalMs = parseInterval(binding.arg)
const update = () => el.textContent = moment(binding.value).fromNow()
update()
el._next_timer = window.setInterval(update, intervalMs)
},
updated(el: RTimeElement, binding) {
if (binding.oldValue !== binding.value) {
if (null != el._next_timer) clearInterval(el._next_timer)
const intervalMs = parseInterval(binding.arg)
const update = () => el.textContent = moment(binding.value).fromNow()
update()
el._next_timer = window.setInterval(update, intervalMs)
}
},
beforeUnmount(el: RTimeElement) {
if (null != el._next_timer) clearInterval(el._next_timer)
}
})
return {}
})

12
ui/app/plugins/toast.ts Normal file
View file

@ -0,0 +1,12 @@
import { defineNuxtPlugin } from '#app'
import Toast, { type PluginOptions } from 'vue-toastification'
import 'vue-toastification/dist/index.css'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(Toast, {
transition: "Vue-Toastification__bounce",
//position: "bottom-right",
maxToasts: 5,
newestOnTop: true,
} as PluginOptions)
})

View file

@ -26,6 +26,7 @@ export const useConfigStore = defineStore('config', () => {
app_commit_sha: '',
app_build_date: '',
app_branch: '',
started: 0,
},
presets: [
{

View file

@ -1,7 +1,7 @@
import { io } from "socket.io-client";
import { ag } from "~/utils/index"
import type { Socket as IOSocket, SocketOptions } from "socket.io-client"
import type { ManagerOptions } from "socket.io-client";
import type { StoreItem } from "~/types/store";
export const useSocketStore = defineStore('socket', () => {
const runtimeConfig = useRuntimeConfig()
@ -65,7 +65,7 @@ export const useSocketStore = defineStore('socket', () => {
on('item_added', stream => {
const json = JSON.parse(stream);
stateStore.add('queue', json.data._id, json.data);
toast.success(`Item queued: ${ag(stateStore.get('queue', json.data._id, {}), 'title')}`);
toast.success(`Item queued: ${ag(stateStore.get('queue', json.data._id, {} as StoreItem), 'title')}`);
});
on(['log_info', 'log_success', 'log_warning', 'log_error'], (event: string, stream: string) => {
@ -111,7 +111,7 @@ export const useSocketStore = defineStore('socket', () => {
return
}
toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {}), 'title')}`);
toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {} as StoreItem), 'title')}`);
if (true === stateStore.has('queue', id)) {
stateStore.remove('queue', id);

View file

@ -1,24 +1,22 @@
import { defineStore } from 'pinia'
import { reactive, toRefs } from 'vue'
import type { StoreItem } from '~/types/store'
type StateType = 'queue' | 'history'
type KeyType = string
type ValueType = StoreItem
interface State {
queue: Record<KeyType, ValueType>
history: Record<KeyType, ValueType>
queue: Record<KeyType, StoreItem>
history: Record<KeyType, StoreItem>
}
export const useStateStore = defineStore('state', () => {
const state = reactive<State>({ queue: {}, history: {} })
const add = (type: StateType, key: KeyType, value: ValueType): void => {
const add = (type: StateType, key: KeyType, value: StoreItem): void => {
state[type][key] = value
}
const update = (type: StateType, key: KeyType, value: ValueType): void => {
const update = (type: StateType, key: KeyType, value: StoreItem): void => {
state[type][key] = value
}
@ -28,7 +26,7 @@ export const useStateStore = defineStore('state', () => {
}
}
const get = (type: StateType, key: KeyType, defaultValue: ValueType | null | object = null): ValueType | null => {
const get = (type: StateType, key: KeyType, defaultValue: StoreItem | null = null): StoreItem | null => {
return state[type][key] || defaultValue
}
@ -40,7 +38,7 @@ export const useStateStore = defineStore('state', () => {
state[type] = {}
}
const addAll = (type: StateType, data: Record<KeyType, ValueType>): void => {
const addAll = (type: StateType, data: Record<KeyType, StoreItem>): void => {
state[type] = data
}

View file

@ -584,16 +584,72 @@ const formatTime = (seconds: number): string => {
*/
const sleep = (seconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, seconds * 1000))
/**
* Waits for the test function to return a truthy value.
*
* @param test - The function to test
* @param timeout_ms - The maximum time to wait in milliseconds.
* @param frequency - The frequency to check the test function in milliseconds.
*
* @returns The result of the test function.
*/
const awaiter = async (test: Function, timeout_ms: number = 20 * 1000, frequency: number = 200) => {
if (typeof (test) != "function") {
throw new Error("test should be a function in awaiter(test, [timeout_ms], [frequency])")
}
const isNotTruthy = (val: any) => val === undefined || val === false || val === null || val.length === 0;
const endTime: number = Date.now() + timeout_ms;
let result = test();
while (isNotTruthy(result)) {
if (Date.now() > endTime) {
return false;
}
await sleep(frequency);
result = test();
}
return result;
}
/**
* Encode a JavaScript object into a URL-safe base64 string.
*
* @param obj - The object to encode.
* @returns A URL-safe base64-encoded string.
*/
const encode = (obj: Record<string, any>): string => {
const jsonStr = JSON.stringify(obj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const binary = String.fromCharCode(...utf8Bytes);
const base64 = btoa(binary);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/**
* Decode a URL-safe base64 string into an object.
*
* @param str - The URL-safe base64-encoded string.
* @returns The decoded JavaScript object.
*/
const decode = (str: string): object => {
const base64 = str
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(str.length + (4 - str.length % 4) % 4, '=');
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const jsonStr = new TextDecoder().decode(bytes);
return JSON.parse(jsonStr);
}
export {
convertCliOptions, separators, getSeparatorsName, iTrim, eTrim, sTrim, ucFirst,
separators, convertCliOptions, getSeparatorsName, iTrim, eTrim, sTrim, ucFirst,
getValue, ag, ag_set, awaitElement, r, copyText, dEvent, makePagination, encodePath,
request, removeANSIColors, dec2hex, makeId, basename, dirname, getQueryParams,
makeDownload,
formatBytes,
has_data,
toggleClass,
cleanObject,
uri,
formatTime,
sleep
makeDownload, formatBytes, has_data, toggleClass, cleanObject, uri, formatTime,
sleep, awaiter, encode, decode
}

View file

@ -5,9 +5,28 @@ function match_str(filterStr: string, dct: Record<string, any>, incomplete: bool
return true;
}
return filterStr
.split(/(?<!\\)&/)
.every(filterPart => _match_one(filterPart.replace(/\\&/g, '&'), dct, incomplete));
if (/\s*&\s*$/.test(filterStr)) {
return false;
}
try {
const filterParts = filterStr
.split(/(?:(^|[^\\]))&/g)
.reduce((parts, part, index, array) => {
if (index % 2 === 0) {
parts.push((part + (array[index + 1] || '')).replace(/\\&/g, '&'));
}
return parts;
}, [] as string[]);
if (filterParts.some(fp => !fp.trim())) {
return false;
}
return filterParts.every(filterPart => _match_one(filterPart, dct, incomplete));
} catch (e) {
return false;
}
}
function lookup_unit_table(unitTable: Record<string, number>, s: string, strict = false): number | null {
@ -101,7 +120,14 @@ function _match_one(filterPart: string, dct: Record<string, any>, incomplete: bo
const opFn = negation ? (a: any, b: any) => !unnegatedOp(a, b) : unnegatedOp;
let value = quoted ?? plain ?? '';
if (quote) value = value.replace(new RegExp(`\\\\${quote}`, 'g'), quote);
if (quote) {
value = value.replace(new RegExp(`\\\\${quote}`, 'g'), quote);
}
if (!(key! in dct)) {
return isIncomplete(key!) || Boolean(noneInclusive);
}
const actual = dct[key!];
let numeric: number | null = null;
@ -117,10 +143,6 @@ function _match_one(filterPart: string, dct: Record<string, any>, incomplete: bo
throw new Error(`Operator ${op} only supports string values!`);
}
if (actual == null) {
return isIncomplete(key!) || Boolean(noneInclusive);
}
return opFn(actual, numeric !== null ? numeric : value);
}
@ -133,12 +155,12 @@ function _match_one(filterPart: string, dct: Record<string, any>, incomplete: bo
const mu = filterPart.trim().match(unaryRe);
if (mu?.groups) {
const { op, key } = mu.groups;
const actual = dct[key!];
if (actual === undefined && isIncomplete(key!)) {
return true;
if (!(key! in dct)) {
return isIncomplete(key!);
}
const actual = dct[key!];
const unaryOp = UNARY_OPERATORS[op!];
if (!unaryOp) {

View file

@ -70,7 +70,11 @@ export default defineNuxtConfig({
},
...extraNitro,
},
vite: {
server: {
allowedHosts: true,
}
},
telemetry: false,
compatibilityDate: "2024-07-13",
})

View file

@ -1,33 +0,0 @@
let observer;
const AUTO_SCROLL_EVENTS = ['scroll', 'mousewheel', 'touchmove'];
let events_handlers = [];
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive('autoscroll', {
mounted: async el => {
let scrolledToBottom = true;
AUTO_SCROLL_EVENTS.forEach((ev, index) => {
events_handlers[index] = () => {
scrolledToBottom = (el.scrollHeight - el.scrollTop) - el.clientHeight <= 80;
}
el.addEventListener(ev, events_handlers[index], { passive: true });
});
observer = new MutationObserver(_ => {
if (false === scrolledToBottom) {
return;
}
el.scrollTop = el.scrollHeight;
});
observer.observe(el, { childList: true, subtree: true });
},
unmounted: el => {
AUTO_SCROLL_EVENTS.forEach((ev, index) => {
el.removeEventListener(ev, events_handlers[index], { passive: true });
});
observer.disconnect();
},
})
});

View file

@ -1,49 +0,0 @@
import { defineNuxtPlugin } from '#app'
import moment from 'moment'
const parseInterval = arg => {
const match = arg.match(/^(\d+)([smhd])$/)
if (!match) {
return 60 * 1000
}
const [, numStr, unit] = match
const num = parseInt(numStr, 10)
switch (unit) {
case 'd': return num * 24 * 3600 * 1000
case 'h': return num * 3600 * 1000
case 'm': return num * 60 * 1000
case 's': return num * 1000
default: return 60 * 1000
}
}
export default defineNuxtPlugin(nuxtApp => nuxtApp.vueApp.directive('rtime', {
mounted(el, binding) {
const intervalMs = binding.arg ? parseInterval(binding.arg) : 60 * 1000
const update = () => el.textContent = moment(binding.value).fromNow()
update()
el._next_timer = window.setInterval(update, intervalMs)
},
updated(el, binding) {
if (binding.oldValue !== binding.value) {
if (null != el._next_timer) {
clearInterval(el._next_timer)
}
const intervalMs = binding.arg ? parseInterval(binding.arg) : 60 * 1000
const update = () => el.textContent = moment(binding.value).fromNow()
update()
el._next_timer = window.setInterval(update, intervalMs)
}
},
beforeUnmount(el) {
if (null != el._next_timer) {
clearInterval(el._next_timer)
}
}
}))

View file

@ -1,9 +0,0 @@
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
export default defineNuxtPlugin(nuxtApp => nuxtApp.vueApp.use(Toast, {
transition: "Vue-Toastification__bounce",
//position: "bottom-right",
maxToasts: 5,
newestOnTop: true
}))

View file

@ -4,7 +4,7 @@
"compilerOptions": {
"allowImportingTsExtensions": false,
"types": [
"./types/globals"
"./app/types/globals"
]
}
}

View file

@ -1,34 +0,0 @@
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// for non arrays, length is undefined, so != 0
const isNotTruthy = (val: any) => val === undefined || val === false || val === null || val.length === 0;
/**
* Waits for the test function to return a truthy value.
*
* @param test - The function to test
* @param timeout_ms - The maximum time to wait in milliseconds.
* @param frequency - The frequency to check the test function in milliseconds.
*
* @returns The result of the test function.
*/
export default async function awaiter(test: Function, timeout_ms: number = 20 * 1000, frequency: number = 200) {
if (typeof (test) != "function") {
throw new Error("test should be a function in awaiter(test, [timeout_ms], [frequency])")
}
const endTime: number = Date.now() + timeout_ms;
let result = test();
while (isNotTruthy(result)) {
if (Date.now() > endTime) {
return false;
}
await sleep(frequency);
result = test();
}
return result;
}

View file

@ -1,21 +0,0 @@
const encode = (obj: Record<string, any>): string => {
const jsonStr = JSON.stringify(obj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const binary = String.fromCharCode(...utf8Bytes);
const base64 = btoa(binary);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const decode = (str: string): object => {
const base64 = str
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(str.length + (4 - str.length % 4) % 4, '=');
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const jsonStr = new TextDecoder().decode(bytes);
return JSON.parse(jsonStr);
}
export { encode, decode }