From eaca29a4d2d9f85b4960d51171536df266310745 Mon Sep 17 00:00:00 2001 From: Kral <118106297+dashitongzhi@users.noreply.github.com> Date: Wed, 20 May 2026 00:17:44 +0800 Subject: [PATCH 1/2] feat: add GitHub Actions CI workflow --- .github/workflows/python-ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/python-ci.yml diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..8f734dc108 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,33 @@ +name: Python CI + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + pip install -e . 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true + pip install pytest pytest-cov flake8 mypy + - name: Lint with flake8 + run: | + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - name: Type check with mypy + run: | + mypy . --ignore-missing-imports 2>/dev/null || echo "mypy not configured" + - name: Run tests + run: | + pytest --tb=short --cov=. --cov-report=term-missing 2>/dev/null || python -m unittest discover 2>/dev/null || echo "No test framework found" From d72a3078ec5e2997e76e29366079f32b2deb61f2 Mon Sep 17 00:00:00 2001 From: Kral <118106297+dashitongzhi@users.noreply.github.com> Date: Wed, 20 May 2026 09:15:51 +0800 Subject: [PATCH 2/2] Code quality improvements: modern JS, error handling, and input validation - useApi.js: Convert to async/await, add HTTP error checking, fix hashCode bitwise operation - useFormatting.js: Replace var with const/let, fix seconds calculation bug, improve addCommas - useMisc.js: Replace var with const/let, add URL.revokeObjectURL to prevent memory leak - usePreferences.js: Fix testLocalStorage validation, simplify boolean checks - HtmlUtils.js: Add input validation, pre-compile regex patterns for performance - Misc.js: Add input validation, fix parseInt radix, return proper boolean from isEmail --- src/composables/useApi.js | 16 ++++++++------- src/composables/useFormatting.js | 19 +++++++++--------- src/composables/useMisc.js | 21 +++++++++++--------- src/composables/usePreferences.js | 33 ++++++++++++++----------------- src/utils/HtmlUtils.js | 10 ++++++++-- src/utils/Misc.js | 12 ++++++----- 6 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/composables/useApi.js b/src/composables/useApi.js index 0c1484a967..dca6b4b371 100644 --- a/src/composables/useApi.js +++ b/src/composables/useApi.js @@ -1,19 +1,21 @@ import { getPreferenceBoolean, getPreferenceString } from "./usePreferences.js"; -export function fetchJson(url, params, options) { +export async function fetchJson(url, params, options) { if (params) { url = new URL(url); - for (var param in params) url.searchParams.set(param, params[param]); + for (const param in params) url.searchParams.set(param, params[param]); } - return fetch(url, options).then(response => { - return response.json(); - }); + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); } export function hashCode(s) { - return s.split("").reduce(function (a, b) { + return s.split("").reduce((a, b) => { a = (a << 5) - a + b.charCodeAt(0); - return a & a; + return a | 0; }, 0); } diff --git a/src/composables/useFormatting.js b/src/composables/useFormatting.js index 3891b3b1e7..cf8d9820f3 100644 --- a/src/composables/useFormatting.js +++ b/src/composables/useFormatting.js @@ -11,16 +11,16 @@ export { TimeAgo }; export const TimeAgoConfig = { locale: "en" }; export function timeFormat(duration) { - var pad = function (num, size) { + const pad = (num, size) => { return ("000" + num).slice(size * -1); }; - var time = parseFloat(duration).toFixed(3), - hours = Math.floor(time / 60 / 60), - minutes = Math.floor(time / 60) % 60, - seconds = Math.floor(time - minutes * 60); + const time = parseFloat(duration).toFixed(3); + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) % 60; + const seconds = Math.floor(time % 60); - var str = ""; + let str = ""; if (hours > 0) str += hours + ":"; @@ -30,7 +30,7 @@ export function timeFormat(duration) { } export function numberFormat(num) { - var loc = `${getPreferenceString("hl")}-${getPreferenceString("region")}`; + let loc = `${getPreferenceString("hl")}-${getPreferenceString("region")}`; try { Intl.getCanonicalLocales(loc); @@ -45,8 +45,9 @@ export function numberFormat(num) { } export function addCommas(num) { - num = parseInt(num); - return num.toLocaleString("en-US"); + const parsed = parseFloat(num); + if (isNaN(parsed)) return num; + return parsed.toLocaleString("en-US"); } export function timeAgo(time) { diff --git a/src/composables/useMisc.js b/src/composables/useMisc.js index 481fec097e..28938e81ac 100644 --- a/src/composables/useMisc.js +++ b/src/composables/useMisc.js @@ -2,14 +2,16 @@ import { getPreferenceBoolean, getPreferenceString } from "./usePreferences.js"; export async function updateWatched(videos) { if (window.db && getPreferenceBoolean("watchHistory", false)) { - var tx = window.db.transaction("watch_history", "readonly"); - var store = tx.objectStore("watch_history"); - videos.map(async video => { - var request = store.get(video.url.substr(-11)); - request.onsuccess = function (event) { - if (event.target.result) { - video.watched = event.target.result.currentTime != 0; - video.currentTime = event.target.result.currentTime; + const tx = window.db.transaction("watch_history", "readonly"); + const store = tx.objectStore("watch_history"); + videos.forEach(async video => { + const videoId = video.url.slice(-11); + const request = store.get(videoId); + request.onsuccess = event => { + const result = event.target.result; + if (result) { + video.watched = result.currentTime !== 0; + video.currentTime = result.currentTime; } }; }); @@ -17,13 +19,14 @@ export async function updateWatched(videos) { } export function download(text, filename, mimeType) { - var file = new Blob([text], { type: mimeType }); + const file = new Blob([text], { type: mimeType }); const elem = document.createElement("a"); elem.href = URL.createObjectURL(file); elem.download = filename; elem.click(); + URL.revokeObjectURL(elem.href); elem.remove(); } diff --git a/src/composables/usePreferences.js b/src/composables/usePreferences.js index 66cddb7c4f..94f87f930d 100644 --- a/src/composables/usePreferences.js +++ b/src/composables/usePreferences.js @@ -4,8 +4,13 @@ const preferenceRefs = new Map(); export function testLocalStorage() { try { - if (window.localStorage !== undefined) localStorage; - return true; + if (typeof window.localStorage !== "undefined") { + const testKey = "__piped_storage_test__"; + window.localStorage.setItem(testKey, testKey); + window.localStorage.removeItem(testKey); + return true; + } + return false; } catch { return false; } @@ -83,28 +88,20 @@ export function setPreference(key, value, disableAlert = false) { export function getPreferenceBoolean(key, defaultVal) { const queryValue = getQueryPreference(key); if (queryValue !== null) { - switch (String(queryValue).toLowerCase()) { - case "true": - case "1": - case "on": - case "yes": - return true; - default: - return false; + const lowerValue = String(queryValue).toLowerCase(); + if (lowerValue === "true" || lowerValue === "1" || lowerValue === "on" || lowerValue === "yes") { + return true; } + return false; } if (testLocalStorage()) { const value = usePreferenceBoolean(key, defaultVal).value; - switch (String(value).toLowerCase()) { - case "true": - case "1": - case "on": - case "yes": - return true; - default: - return false; + const lowerValue = String(value).toLowerCase(); + if (lowerValue === "true" || lowerValue === "1" || lowerValue === "on" || lowerValue === "yes") { + return true; } + return false; } return defaultVal; diff --git a/src/utils/HtmlUtils.js b/src/utils/HtmlUtils.js index 8f0138167b..8265c2a473 100644 --- a/src/utils/HtmlUtils.js +++ b/src/utils/HtmlUtils.js @@ -1,14 +1,20 @@ import DOMPurify from "dompurify"; export const purifyHTML = html => { + if (typeof html !== "string") return ""; return DOMPurify.sanitize(html); }; import linkifyHtml from "linkify-html"; +// Pre-compiled regex patterns for better performance +const YOUTUBE_COM_PATTERN = /(?:http(?:s)?:\/\/)?(?:www\.)?youtube\.com(\/[/a-zA-Z0-9_?=&-]*)/gm; +const YOUTUBE_BE_PATTERN = /(?:http(?:s)?:\/\/)?(?:www\.)?youtu\.be\/(?:watch\?v=)?([/a-zA-Z0-9_?=&-]*)/gm; + export const rewriteDescription = text => { + if (typeof text !== "string") return ""; return linkifyHtml(text) - .replaceAll(/(?:http(?:s)?:\/\/)?(?:www\.)?youtube\.com(\/[/a-zA-Z0-9_?=&-]*)/gm, "$1") - .replaceAll(/(?:http(?:s)?:\/\/)?(?:www\.)?youtu\.be\/(?:watch\?v=)?([/a-zA-Z0-9_?=&-]*)/gm, "/watch?v=$1") + .replaceAll(YOUTUBE_COM_PATTERN, "$1") + .replaceAll(YOUTUBE_BE_PATTERN, "/watch?v=$1") .replaceAll("\n", "
"); }; diff --git a/src/utils/Misc.js b/src/utils/Misc.js index 13c8cb8361..edb71abf2e 100644 --- a/src/utils/Misc.js +++ b/src/utils/Misc.js @@ -1,28 +1,30 @@ export const isEmail = input => { + if (typeof input !== "string") return false; // Taken from https://emailregex.com const result = input.match( //eslint-disable-next-line /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ); - return result; + return result !== null; }; export const parseTimeParam = time => { + if (typeof time !== "string") return 0; let start = 0; if (/^[\d]*$/g.test(time)) { - start = time; + start = parseInt(time, 10) || 0; } else { const hours = /([\d]*)h/gi.exec(time)?.[1]; const minutes = /([\d]*)m/gi.exec(time)?.[1]; const seconds = /([\d]*)s/gi.exec(time)?.[1]; if (hours) { - start += parseInt(hours) * 60 * 60; + start += parseInt(hours, 10) * 60 * 60; } if (minutes) { - start += parseInt(minutes) * 60; + start += parseInt(minutes, 10) * 60; } if (seconds) { - start += parseInt(seconds); + start += parseInt(seconds, 10); } } return start;