smoothie/src/lib/blenderfetch.js
2026-03-09 20:15:49 +01:00

203 lines
7.0 KiB
JavaScript

import { fetch } from '@tauri-apps/plugin-http';
import { platform } from '@tauri-apps/plugin-os';
import { currentSettings } from './settings';
import { writable } from 'svelte/store';
import { get } from 'svelte/store';
/**
* @typedef {Object} FileLink
* @property {string} version - The version string (e.g., "5.0.0")
* @property {string} downloadUrl - The full download URL
* @property {string} platform - The platform identifier (e.g., 'linux', 'windows', 'macos')
* @property {string} architecture - The architecture identifier (e.g., 'x64', 'arm64')
* @property {string} fileExtension - The file extension (e.g., 'tar.xz', 'zip')
* @property {string} status - The download status ('pending', 'downloading', 'completed', 'error')
* @property {number} percent - The download progress percentage (0-100)
*/
/**
* @typedef {Object} BlenderRelease
* @property {string} original - The original href from the release page
* @property {string} version - The version string (e.g., "2.80", "3.6", "4.2alpha")
* @property {string} fullPath - The full URL to the release folder
* @property {Date|null} lastModified - The last modified date (currently null)
* @property {Array<FileLink>} links - Array of download links for this release
*/
export const blenderReleases = writable([]);
export async function getBlenderReleases() {
const settings = await get(currentSettings);
const currentPlatform = settings.defaultPlatform || platform();
const currentArch = settings.defaultArch;
try {
// Fetch the HTML page
const response = await fetch('https://download.blender.org/release/');
const html = await response.text();
// Parse the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract all links that look like version folders
const links = doc.querySelectorAll('a');
/** @type {Promise<any>[]} */
const folderPromises = [];
links.forEach((link) => {
const href = link.getAttribute('href');
// Filter for directory links (ending with '/') that are version folders
// This regex matches patterns like Blender2.80/, Blender3.6/, Blender4.2alpha/, etc.
if (href && href.endsWith('/') && href.match(/^Blender[\d.]+[a-z]*\//)) {
// Remove the trailing slash and 'Blender' prefix for cleaner version names
const versionName = href.slice(0, -1).replace('Blender', ''); // Remove trailing slash
if (parseFloat(versionName) > 2.78) {
const promise = getBlenderMinorVersionsWithDownload(
`https://download.blender.org/release/${href}`,
currentPlatform,
currentArch
).then((links) => {
// Only add to array if there are links
if (links && links.length > 0) {
return {
original: href,
version: versionName, // e.g., "2.80", "3.6", "4.2alpha"
fullPath: `https://download.blender.org/release/${href}`,
lastModified: null, // Date info is in a separate column, harder to parse
links: links
};
}
return null;
});
folderPromises.push(promise);
}
}
});
const folders = (await Promise.all(folderPromises)).filter((folder) => folder !== null);
folders.sort((a, b) => compareVersions(b.version, a.version));
blenderReleases.set(folders);
return folders;
} catch (error) {
console.error('Error fetching Blender release list:', error);
return [];
}
}
/**
* Fetches the minor versions (e.g., 5.0.0, 5.1.0) for a given major version folder URL,
* along with their download URLs for the current platform and architecture.
* @returns {Promise<FileLink[]>} List of minor versions with download URLs
* @param {string} majorVersionFolderUrl
* @param {string} currentPlatform
* @param {string} currentArch
*/
async function getBlenderMinorVersionsWithDownload(
majorVersionFolderUrl,
currentPlatform,
currentArch
) {
try {
// 1. Fetch the HTML of the version folder (e.g., Blender5.0/)
const response = await fetch(majorVersionFolderUrl);
const html = await response.text();
// 2. Parse the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 3. Map your platform/arch to the naming convention used in filenames
/** @type {{ linux: string; windows: string; darwin: string; }} */
const platformMap = {
linux: 'linux',
windows: 'windows',
darwin: 'macos' // macOS is called 'darwin' in some contexts, but files use 'macos'
};
/** @type {{ x86_64: string; arm64: string; }} */
const archMap = {
x86_64: 'x64',
arm64: 'arm64'
};
const targetPlatform =
platformMap[/** @type {keyof platformMap} */ (currentPlatform)] || currentPlatform;
const targetArch = archMap[/** @type {keyof archMap} */ (currentArch)] || currentArch;
// 4. Get all file links and process them
const links = doc.querySelectorAll('a');
/** @type {Map<string, { [key: string]: { url: string; extension: string; platform: string; arch: string } }>} */
const versionsMap = new Map(); // Key: version string, Value: object with files
links.forEach((link) => {
const href = link.getAttribute('href');
if (!href) return;
// Match pattern: blender-5.0.0-linux-x64.tar.xz
// Regex captures: version, platform, arch, extension
const match = href.match(/^blender-([\d.]+)-([a-z]+)-([a-z0-9]+)\.([a-z.]+)$/);
if (match) {
const [_, version, platform, arch, ext] =
/** @type {[string, string, string, string, string]} */ (match);
// Ignore checksum files (.md5, .sha256) if you don't need them
if (ext === 'md5' || ext === 'sha256') return;
// Store file info grouped by version
if (!versionsMap.has(version)) {
versionsMap.set(version, {});
}
const versionFiles = versionsMap.get(version);
if (!versionFiles) return;
const key = `${platform}-${arch}`;
versionFiles[key] = {
url: new URL(href, majorVersionFolderUrl).href,
extension: ext,
platform,
arch
};
}
});
// 5. For each version, pick the correct download link based on target platform/arch
const results = [];
const targetKey = `${targetPlatform}-${targetArch}`;
for (const [version, files] of versionsMap.entries()) {
const fileInfo = files[targetKey];
if (fileInfo) {
results.push({
version: version,
downloadUrl: fileInfo.url,
platform: targetPlatform,
architecture: targetArch,
fileExtension: fileInfo.extension,
status: 'pending',
percent: 0
});
} else {
console.warn(`No matching file found for ${version} with ${targetKey}`);
}
}
return results.sort((a, b) => compareVersions(a.version, b.version)); // Optional: sort versions
} catch (error) {
console.error('Error fetching Blender versions:', error);
return [];
}
}
// Helper function to compare version strings (e.g., "5.0.0", "5.0.1")
/**
* @param {string} v1
* @param {string} v2
*/
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;
if (num1 !== num2) return num1 - num2;
}
return 0;
}