From 517f60f49d7ff2907bea47a124ca5ef256853614 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Thu, 5 Jun 2025 23:47:00 +0300 Subject: [PATCH 01/22] Added ts implementation of yt-dlp match_str --- ui/components/ConditionForm.vue | 13 ++-- ui/utils/ytdlp.ts | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 ui/utils/ytdlp.ts diff --git a/ui/components/ConditionForm.vue b/ui/components/ConditionForm.vue index 49afd01e..a39dbc3c 100644 --- a/ui/components/ConditionForm.vue +++ b/ui/components/ConditionForm.vue @@ -71,6 +71,11 @@
- - The yt-dlp [--match-filters] filter logic. - - Test filter logic - - + yt-dlp [--match-filters] logic.
diff --git a/ui/utils/ytdlp.ts b/ui/utils/ytdlp.ts new file mode 100644 index 00000000..efbb7ac5 --- /dev/null +++ b/ui/utils/ytdlp.ts @@ -0,0 +1,107 @@ +const NUMBER_RE = /\d+(?:\.\d+)?/; + +function match_str(filterStr: string, dct: Record, incomplete: boolean | Set = false): boolean { + return filterStr + .split(/(? _match_one(filterPart.replace(/\\&/g, '&'), dct, incomplete)); +} + +function lookup_unit_table(unitTable: Record, s: string, strict = false): number | null { + const numRe = strict ? NUMBER_RE : /\d+[,.]?\d*/; + const unitsRe = Object.keys(unitTable).map(u => u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); + const re = new RegExp(`^(?${numRe.source})\s*(?${unitsRe})\b`, strict ? '' : 'i'); + + const m = s.match(re); + if (!m || !m.groups) return null; + + const num = parseFloat(m.groups.num.replace(',', '.')); + const mult = unitTable[m.groups.unit]; + + return Math.round(num * mult); +} + +function parse_filesize(s: string | null): number | null { + if (!s) return null; + + const UNIT_TABLE: Record = { + B: 1, bytes: 1, b: 1, + KiB: 1024, KB: 1000, kB: 1024, kb: 1000, kilobytes: 1000, kibibytes: 1024, + MiB: 1024 ** 2, MB: 1000 ** 2, megabytes: 1000 ** 2, mebibytes: 1024 ** 2, + GiB: 1024 ** 3, GB: 1000 ** 3, gigabytes: 1000 ** 3, gibibytes: 1024 ** 3, + TiB: 1024 ** 4, TB: 1000 ** 4, terabytes: 1000 ** 4, tebibytes: 1024 ** 4, + }; + + return lookup_unit_table(UNIT_TABLE, s); +} + +function parse_duration(s: string): number | null { + if (!s.trim()) return null; + + const durationRe = /^(?:(?:(?\d+):)?(?\d+):)?(?\d+):(?\d{1,2})(?[.:]\d+)?Z?$/; + const m = s.match(durationRe); + if (!m || !m.groups) return null; + + const { days, hours, mins, secs, ms } = m.groups; + + return ( + (parseFloat(days || '0') * 86400) + + (parseFloat(hours || '0') * 3600) + + (parseFloat(mins || '0') * 60) + + parseFloat(secs || '0') + + parseFloat((ms || '0').replace(':', '.')) + ); +} + +function _match_one(filterPart: string, dct: Record, incomplete: boolean | Set): boolean { + const STRING_OPERATORS: Record = { + '*=': (a: string, v: string) => a.includes(v), + '^=': (a: string, v: string) => a.startsWith(v), + '$=': (a: string, v: string) => a.endsWith(v), + '~=': (a: string, v: string) => new RegExp(v).test(a), + }; + + const COMPARISON_OPERATORS: Record = { + ...STRING_OPERATORS, + '<=': (a: any, b: any) => a <= b, + '<': (a: any, b: any) => a < b, + '>=': (a: any, b: any) => a >= b, + '>': (a: any, b: any) => a > b, + '=': (a: any, b: any) => a == b, + }; + + const match = filterPart.trim().match(/^([a-z_]+)\s*(!)?\s*([*^$~=<>]+)\s*(?:"(.+)"|'(.+)'|(.+))$/); + + if (match) { + const [, key, negation, op, qstr, sstr, ustr] = match; + const value = qstr || sstr || ustr; + const actual = dct[key]; + + const compValue = parse_filesize(value) || parse_duration(value) || value; + + const _cmp = COMPARISON_OPERATORS[op]; + if (actual === undefined) return incomplete === true || (incomplete instanceof Set && incomplete.has(key)); + + return negation ? !_cmp(actual, compValue) : _cmp(actual, compValue); + } + + const UNARY_OPERATORS: Record = { + '': (v: any) => v != null, + '!': (v: any) => v == null, + }; + + const unaryMatch = filterPart.trim().match(/^(!)?([a-z_]+)$/); + + if (unaryMatch) { + const [, op, key] = unaryMatch; + const actual = dct[key]; + + if (actual === undefined && (incomplete === true || (incomplete instanceof Set && incomplete.has(key)))) return true; + + return UNARY_OPERATORS[op || ''](actual); + } + + throw new Error(`Invalid filter part '${filterPart}'`); +} + + +export { match_str, lookup_unit_table, parse_filesize, parse_duration, _match_one } From 9419e15a4f5bdc3aeed8dd494f4c8824a5cf33a8 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 6 Jun 2025 01:33:29 +0300 Subject: [PATCH 02/22] minor changes to changelog page --- app/library/Notifications.py | 4 +- app/library/config.py | 59 +++++++++++++++++++++++ ui/pages/changelog.vue | 90 +++++++++++++++++++++--------------- 3 files changed, 113 insertions(+), 40 deletions(-) diff --git a/app/library/Notifications.py b/app/library/Notifications.py index 45ab1182..3c7d93af 100644 --- a/app/library/Notifications.py +++ b/app/library/Notifications.py @@ -17,7 +17,6 @@ from .Events import Event, EventBus, Events from .ItemDTO import ItemDTO from .Singleton import Singleton from .Utils import validate_uuid -from .version import APP_VERSION LOG = logging.getLogger("notifications") @@ -138,6 +137,7 @@ class Notification(metaclass=Singleton): self._file: str = file or os.path.join(config.config_path, "notifications.json") self._client: httpx.AsyncClient = client or httpx.AsyncClient() self._encoder: Encoder = encoder or Encoder() + self._version = config.version if os.path.exists(self._file): try: @@ -360,7 +360,7 @@ class Notification(metaclass=Singleton): "method": target.request.method.upper(), "url": target.request.url, "headers": { - "User-Agent": f"YTPTube/{APP_VERSION}", + "User-Agent": f"YTPTube/{self._version}", "X-Event-Id": ev.id, "X-Event": ev.event, "Content-Type": "application/json" diff --git a/app/library/config.py b/app/library/config.py index 195b7b0d..48f82934 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -421,6 +421,9 @@ class Config: logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.INFO) + if self.version == "dev-master": + self._version_via_git() + def _get_attributes(self) -> dict: attrs: dict = {} vClass: str = self.__class__ @@ -471,3 +474,59 @@ class Config: return YTDLP_VERSION except ImportError: return "0.0.0" + + def _version_via_git(self): + """ + Updates the version of the application using git tags. + This is used to set the version to the latest git tag. + """ + git_path: str = os.path.join(os.path.dirname(__file__), "..", "..", ".git") + if not os.path.exists(git_path): + logging.warning(f"Git directory '{git_path}' does not exist. Cannot determine version.") + return + + try: + import subprocess + + branch_result = subprocess.run( # noqa: S603 + ["git", "rev-parse", "--abbrev-ref", "HEAD"], # noqa: S607 + cwd=os.path.dirname(git_path), + capture_output=True, + text=True, + check=False, + ) + + if 0 != branch_result.returncode: + logging.error(f"Git rev-parse failed: {branch_result.stderr.strip()}") + return + + branch_name: str = branch_result.stdout.strip() + if not branch_name: + logging.warning("Git branch name is empty.") + return + + commit_result = subprocess.run( # noqa: S603 + ["git", "log", "-1", "--format=%ct_%H"], # noqa: S607 + cwd=os.path.dirname(git_path), + capture_output=True, + text=True, + check=False, + ) + + if 0 != commit_result.returncode: + logging.error(f"Git log failed: {commit_result.stderr.strip()}") + return + + commit_info: str = commit_result.stdout.strip() + if not commit_info: + logging.warning("Git commit info is empty.") + return + + commit_date, commit_sha = commit_info.split("_", 1) + commit_date = time.strftime("%Y%m%d", time.localtime(int(commit_date))) + commit_sha = commit_sha[:8] + + self.version = f"{branch_name}-{commit_date}-{commit_sha}" + logging.info(f"Application version set to '{self.version}' based on git data.") + except Exception as e: + logging.error(f"Error while getting git version: {e!s}") diff --git a/ui/pages/changelog.vue b/ui/pages/changelog.vue index f6204b33..9b868f60 100644 --- a/ui/pages/changelog.vue +++ b/ui/pages/changelog.vue @@ -23,19 +23,19 @@