Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/lib/pad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/** Builds the PAD (Portable Application Description) file that software directories such as UpdateStar read. */

type PadValue = string | { [element: string]: PadValue };

const email = '[email protected]';
const sourceUrl = 'https://github.com/OpenTubeX/OpenTubeX';
const rawSourceUrl = 'https://raw.githubusercontent.com/OpenTubeX/OpenTubeX/development';

// Languages of the app's active locales, as PAD 4.0 language identifiers.
const languages = [
'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', 'Dutch', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Slovak', 'Slovenian', 'Serbian', 'Swedish',
'Tamil', 'Turkish', 'Ukrainian', 'Vietnamese', 'ChineseSimplified', 'ChineseTraditional',
];

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<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' };
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)}</${name}>`;
const children = Object.entries(value).map(([child, childValue]) => render(child, childValue, `${indent}\t`));
return [`${indent}<${name}>`, ...children, `${indent}</${name}>`].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',
// 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',
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: '',
},
},
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 `<?xml version="1.0" encoding="UTF-8"?>\n${render('XML_DIZ_INFO', pad, '')}\n`;
}
16 changes: 16 additions & 0 deletions src/pages/pad.xml.ts
Original file line number Diff line number Diff line change
@@ -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' } });
};
36 changes: 36 additions & 0 deletions tests/pad.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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('<?xml version="1.0" encoding="UTF-8"?>\n<XML_DIZ_INFO>\n');
expect(xml).toContain('<Program_Version>1.2.3</Program_Version>');
expect(xml).toContain('<Program_Release_Month>10</Program_Release_Month>');
expect(xml).toContain('<Program_Release_Day>03</Program_Release_Day>');
expect(xml).toContain('<Program_Release_Year>2026</Program_Release_Year>');
expect(xml).toContain('<Primary_Download_URL>https://example.org/setup.exe?a=1&amp;b=2</Primary_Download_URL>');
expect(xml).toContain('<Program_Category_Class>Audio &amp; Multimedia::Video Tools</Program_Category_Class>');
expect(xml).toContain('<Application_XML_File_URL>https://opentubex.org/pad.xml</Application_XML_File_URL>');
expect(xml).toContain('<Program_OS_Support>WinOther,Mac OS X,Linux,Android</Program_OS_Support>');
expect(xml).toContain('<Additional_Download_URL_1></Additional_Download_URL_1>');
});

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('<Program_Release_Year></Program_Release_Year>');
});
Loading