From b3ddab11c254ce322b094e1542e3af792ced5b07 Mon Sep 17 00:00:00 2001 From: D3SOX Date: Mon, 26 Oct 2026 11:14:16 +0100 Subject: [PATCH 1/2] feat: publish a PAD file so software directories can list OpenTubeX UpdateStar and similar directories add software from a hosted PAD file. The site generates it at build time from the release data, so the listed version updates with every release deploy. --- src/lib/pad.ts | 106 +++++++++++++++++++++++++++++++++++++++++++ src/pages/pad.xml.ts | 16 +++++++ tests/pad.test.ts | 34 ++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/lib/pad.ts create mode 100644 src/pages/pad.xml.ts create mode 100644 tests/pad.test.ts diff --git a/src/lib/pad.ts b/src/lib/pad.ts new file mode 100644 index 0000000..3d7c5ba --- /dev/null +++ b/src/lib/pad.ts @@ -0,0 +1,106 @@ +/** Builds the PAD (Portable Application Description) file that software directories such as UpdateStar read. */ + +type PadValue = string | { [element: string]: PadValue }; + +const email = 'info@opentubex.org'; +const sourceUrl = 'https://github.com/OpenTubeX/OpenTubeX'; +const rawSourceUrl = 'https://raw.githubusercontent.com/OpenTubeX/OpenTubeX/development'; + +// Languages of the app's active locales. +const languages = [ + 'English', 'Arabic', 'Azerbaijani', 'Belarusian', 'Bulgarian', 'Breton', 'Catalan', 'Czech', 'Welsh', + 'Danish', 'German', 'Greek', 'Spanish', 'Estonian', 'Basque', 'Persian', 'Finnish', 'French', 'Galician', + 'Hebrew', 'Hungarian', 'Croatian', 'Indonesian', 'Icelandic', 'Italian', 'Japanese', 'Korean', 'Lithuanian', + 'Norwegian Bokmål', 'Dutch', 'Norwegian Nynorsk', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Slovak', + 'Slovenian', 'Serbian', 'Swedish', 'Tamil', 'Turkish', 'Ukrainian', 'Vietnamese', 'Chinese', +]; + +export const padDescriptions = { + Char_Desc_45: 'Private YouTube client without ads', + Char_Desc_80: 'Open source YouTube client without ads or a Google account', + Char_Desc_250: + 'OpenTubeX is a free and open source YouTube client for Windows, macOS, Linux and Android. It needs no Google account, shows no ads, and keeps subscriptions, playlists and history on your device.', + Char_Desc_450: + 'OpenTubeX is a free and open source YouTube client for Windows, macOS, Linux and Android. It needs no Google account, shows no ads, and keeps subscriptions, playlists and history on your device. Videos load through a built-in extractor, an Invidious instance or a Yattee Server. SponsorBlock and DeArrow are built in.', + Char_Desc_2000: `OpenTubeX is a free and open source YouTube client for Windows, macOS, Linux and Android. It needs no Google account, shows no ads, and keeps subscriptions, playlists and watch history on your device by default. + +Videos load through a built-in extractor, an Invidious instance or a Yattee Server, and the app can send its traffic through a proxy such as Tor. SponsorBlock and DeArrow are built in. + +You can group subscriptions into profiles, choose whether each channel shows videos, Shorts, live streams or posts, and set daily video limits. You can save playback speed and quality per channel and customize keyboard shortcuts. Videos open in tabs, and the queue and mini-player keep playing while you browse. + +Optional sync keeps your library the same on all your devices, with end-to-end encryption on a compatible server. OpenTubeX supports custom themes and is available in more than 40 languages. + +OpenTubeX is licensed under the GNU Affero General Public License v3.0.`, +}; + +function escapeXml(text: string): string { + const entities: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; + return text.replace(/[&<>"']/g, (char) => entities[char]!); +} + +function render(name: string, value: PadValue, indent: string): string { + if (typeof value === 'string') return `${indent}<${name}>${escapeXml(value)}`; + const children = Object.entries(value).map(([child, childValue]) => render(child, childValue, `${indent}\t`)); + return [`${indent}<${name}>`, ...children, `${indent}`].join('\n'); +} + +export function padXml({ site, version, publishedAt, downloadUrl }: { + site: URL; + version: string; + publishedAt?: string; + downloadUrl: string; +}): string { + const released = publishedAt ? new Date(publishedAt) : undefined; + const pad: PadValue = { + MASTER_PAD_VERSION_INFO: { + MASTER_PAD_VERSION: '4.0', + MASTER_PAD_INFO: 'Portable Application Description, or PAD for short, is a data set that is used by software authors to describe their software products.', + }, + Company_Info: { + Company_Name: 'OpenTubeX', + Company_WebSite_URL: site.href, + Contact_Info: { Contact_Email: email, Author_Email: email }, + Support_Info: { Sales_Email: email, Support_Email: email, General_Email: email }, + }, + Program_Info: { + Program_Name: 'OpenTubeX', + Program_Version: version, + Program_Release_Month: released ? String(released.getUTCMonth() + 1).padStart(2, '0') : '', + Program_Release_Day: released ? String(released.getUTCDate()).padStart(2, '0') : '', + Program_Release_Year: released ? String(released.getUTCFullYear()) : '', + Program_Cost_Dollars: '0', + Program_Type: 'Freeware', + Program_Release_Status: 'Beta', + Program_Install_Support: 'Install and Uninstall', + Program_OS_Support: 'Windows 10,Windows 11,Mac OS X,Linux,Android', + Program_Language: languages.join(','), + Program_Category_Class: 'Audio & Multimedia::Video Tools', + Program_System_Requirements: 'Windows 10 or later, macOS 12 or later, Linux, or Android 8.0 or later', + Expire_Info: { Has_Expire_Info: 'N' }, + }, + Web_Info: { + Application_URLs: { + Application_Info_URL: site.href, + Application_Order_URL: '', + Application_Screenshot_URL: `${rawSourceUrl}/docs/screenshots/OpenTubeX1-dark.png`, + Application_Icon_URL: `${rawSourceUrl}/_icons/iconColor.png`, + Application_XML_File_URL: new URL('/pad.xml', site).href, + }, + Download_URLs: { + Primary_Download_URL: downloadUrl, + Additional_Download_URL_1: new URL('/downloads/', site).href, + }, + }, + Program_Descriptions: { + English: { + Keywords: 'youtube, youtube client, video player, privacy, no ads, sponsorblock, invidious, open source', + ...padDescriptions, + }, + }, + Permissions: { + Distribution_Permissions: `OpenTubeX is free software under the GNU Affero General Public License v3.0. You may redistribute it under the terms of that license. Source code: ${sourceUrl}`, + EULA: `GNU Affero General Public License v3.0: ${sourceUrl}/blob/development/LICENSE`, + }, + }; + return `\n${render('XML_DIZ_INFO', pad, '')}\n`; +} diff --git a/src/pages/pad.xml.ts b/src/pages/pad.xml.ts new file mode 100644 index 0000000..4b64418 --- /dev/null +++ b/src/pages/pad.xml.ts @@ -0,0 +1,16 @@ +import type { APIRoute } from 'astro'; +import { recommendedDownload, releaseTag, releaseVersion } from '../data/release'; +import { getChangelogReleases } from '../lib/github-releases'; +import { padXml } from '../lib/pad'; + +export const GET: APIRoute = async ({ site }) => { + const releases = await getChangelogReleases(); + const xml = padXml({ + site: site ?? new URL('https://opentubex.org'), + version: releaseVersion, + publishedAt: releases.find(({ tag }) => tag === releaseTag)?.publishedAt, + downloadUrl: recommendedDownload('Windows', 'x86')!.url, + }); + + return new Response(xml, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } }); +}; diff --git a/tests/pad.test.ts b/tests/pad.test.ts new file mode 100644 index 0000000..8028664 --- /dev/null +++ b/tests/pad.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from 'bun:test'; +import { padDescriptions, padXml } from '../src/lib/pad'; + +const site = new URL('https://opentubex.org'); + +test('PAD descriptions fit their character limits', () => { + for (const [field, text] of Object.entries(padDescriptions)) { + expect(text.length).toBeLessThanOrEqual(Number(field.replace('Char_Desc_', ''))); + } +}); + +test('PAD file describes the listed release', () => { + const xml = padXml({ + site, + version: '1.2.3', + publishedAt: '2026-10-03T16:29:18Z', + downloadUrl: 'https://example.org/setup.exe?a=1&b=2', + }); + + expect(xml).toStartWith('\n\n'); + expect(xml).toContain('1.2.3'); + expect(xml).toContain('10'); + expect(xml).toContain('03'); + expect(xml).toContain('2026'); + expect(xml).toContain('https://example.org/setup.exe?a=1&b=2'); + expect(xml).toContain('Audio & Multimedia::Video Tools'); + expect(xml).toContain('https://opentubex.org/pad.xml'); +}); + +test('PAD file leaves the release date empty when it is unknown', () => { + const xml = padXml({ site, version: '1.2.3', downloadUrl: 'https://example.org/setup.exe' }); + + expect(xml).toContain(''); +}); From 9d986e8c6a7319201c143b2139c4744ab4e52694 Mon Sep 17 00:00:00 2001 From: D3SOX Date: Mon, 26 Oct 2026 11:23:39 +0100 Subject: [PATCH 2/2] fix: use PAD 4.0 identifiers for OS, languages and download URLs --- src/lib/pad.ts | 13 +++++++------ tests/pad.test.ts | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/pad.ts b/src/lib/pad.ts index 3d7c5ba..4782397 100644 --- a/src/lib/pad.ts +++ b/src/lib/pad.ts @@ -6,13 +6,13 @@ const email = 'info@opentubex.org'; const sourceUrl = 'https://github.com/OpenTubeX/OpenTubeX'; const rawSourceUrl = 'https://raw.githubusercontent.com/OpenTubeX/OpenTubeX/development'; -// Languages of the app's active locales. +// Languages of the app's active locales, as PAD 4.0 language identifiers. const languages = [ - 'English', 'Arabic', 'Azerbaijani', 'Belarusian', 'Bulgarian', 'Breton', 'Catalan', 'Czech', 'Welsh', + 'English', 'Arabic', 'Azerbaijani', 'Byelorussian', 'Bulgarian', 'Breton', 'Catalan', 'Czech', 'Welsh', 'Danish', 'German', 'Greek', 'Spanish', 'Estonian', 'Basque', 'Persian', 'Finnish', 'French', 'Galician', 'Hebrew', 'Hungarian', 'Croatian', 'Indonesian', 'Icelandic', 'Italian', 'Japanese', 'Korean', 'Lithuanian', - 'Norwegian Bokmål', 'Dutch', 'Norwegian Nynorsk', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Slovak', - 'Slovenian', 'Serbian', 'Swedish', 'Tamil', 'Turkish', 'Ukrainian', 'Vietnamese', 'Chinese', + 'Norwegian', 'Dutch', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Slovak', 'Slovenian', 'Serbian', 'Swedish', + 'Tamil', 'Turkish', 'Ukrainian', 'Vietnamese', 'ChineseSimplified', 'ChineseTraditional', ]; export const padDescriptions = { @@ -72,7 +72,8 @@ export function padXml({ site, version, publishedAt, downloadUrl }: { Program_Type: 'Freeware', Program_Release_Status: 'Beta', Program_Install_Support: 'Install and Uninstall', - Program_OS_Support: 'Windows 10,Windows 11,Mac OS X,Linux,Android', + // The PAD 4.0 OS list predates Windows 10, so WinOther covers it. + Program_OS_Support: 'WinOther,Mac OS X,Linux,Android', Program_Language: languages.join(','), Program_Category_Class: 'Audio & Multimedia::Video Tools', Program_System_Requirements: 'Windows 10 or later, macOS 12 or later, Linux, or Android 8.0 or later', @@ -88,7 +89,7 @@ export function padXml({ site, version, publishedAt, downloadUrl }: { }, Download_URLs: { Primary_Download_URL: downloadUrl, - Additional_Download_URL_1: new URL('/downloads/', site).href, + Additional_Download_URL_1: '', }, }, Program_Descriptions: { diff --git a/tests/pad.test.ts b/tests/pad.test.ts index 8028664..4c289ce 100644 --- a/tests/pad.test.ts +++ b/tests/pad.test.ts @@ -25,6 +25,8 @@ test('PAD file describes the listed release', () => { expect(xml).toContain('https://example.org/setup.exe?a=1&b=2'); expect(xml).toContain('Audio & Multimedia::Video Tools'); expect(xml).toContain('https://opentubex.org/pad.xml'); + expect(xml).toContain('WinOther,Mac OS X,Linux,Android'); + expect(xml).toContain(''); }); test('PAD file leaves the release date empty when it is unknown', () => {