Vue normale

Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.
À partir d’avant-hierFlux principal

GridTV, le guide TV open source pour votre setup IPTV

Par : Korben ✨
7 mai 2026 à 10:30

Bon, maintenant que vous avez vos chaînes IPTV qui tournent via Tunarr ou xTeVe, votre flux XMLTV est super propre. Mais il vous manque un seul truc : Un guide de programme potable.

Hé bien GridTV développé par l'ami JohnnyBeGood est là pour ça !

GridTV c'est une interface web en PHP/JS/CSS qui transforme toute source XMLTV compatible en guide TV façon grille horizontale, avec l'indicateur "maintenant" visible en permanence, une barre de progression du programme en cours, et les émissions passées qui se retrouvent automatiquement grisées. C'est exactement ce à quoi ressemble le guide TV de votre box opérateur, mais en mieux, et pour votre propre contenu !

Pour le déploiement, Docker est le chemin recommandé plutôt que de tout configurer à la main : git clone, cd GridTV, docker compose up -d, et hop, vous ouvrez localhost:8080.

Un assistant de setup vous demandera alors votre source EPG obligatoire et une playlist M3U si notamment vous voulez utiliser le player intégré, et une fois validé, vous retombez directement sur la grille.

Ça se met en place en moins de 5 min mais si vous préférez installer sans Docker, ou plutôt sans la couche conteneur, il y a également sur le Github des exemples de config pour Apache et Nginx dans la doc. Caddy fonctionnera aussi et la doc concernant Traefik, c'est pour le cas où GridTV tourne en Docker mais derrière un reverse proxy.

Côté fonctionnalités, le player HLS s'ouvre en PiP (Picture in Picture) dans un coin en cliquant sur une chaîne et le multi-EPG vous permettra de configurer plusieurs sources avec un petit switch. GridTV propose aussi des rappels de programme via notifications navigateur, 15 minutes avant la diffusion. Mais pour en profiter, l'onglet du browser doit rester ouvert et les notifs autorisées.

Et il y a aussi possibilité de générer un export PDF/PNG du guide sur 24h. C'est pas indispensable mais ça permet pour ceux qui veulent d'imprimer le programme de la soirée.

Chaque visiteur de l'instance peut aussi utiliser / paramétrer ses propres URLs XMLTV/M3U, car rien n'est stocké côté serveur. Hé oui, tout passe par le localStorage du navigateur donc vous pouvez partager votre instance avec autant de monde que vous voulez, ça n'a pas d'impact.

La version Steampunk

Et il y a même des thèmes genre cyberpunk, steampunk, magazine ou le thème par défaut. Et la page de monitoring admin expose également une sonde accessible via un endpoint compatible Uptime Kuma qui renvoie le code HTTP 200 si tout va bien. Sinon, ce sera du code 503. Bref, ça vous connaissez...

Bref, l'outil est jeune mais bien construit et une démo live tourne ici guide.demo.johnnybegood.fr . À suivre donc....

Et si vous cherchez juste des listes de chaînes IPTV gratuites , c'est par là !

sandbox-exec - L'outil de sandboxing caché de votre Mac

Par : Korben
16 avril 2026 à 09:00

Sandbox-exec, c'est un utilitaire en ligne de commande dont pas grand monde ne parle mais qui est intégré à macOS et qui permet de lancer n'importe quel programme dans un bac à sable sécurisé, avec des restrictions sur mesure. Apple l'a déprécié, mais ça marche toujours... et c'est franchement pratique.

Avec ce truc, il suffit de créer un petit fichier de profil (extension .sb) et vous lancez votre commande avec sandbox-exec -f profil.sb votre_commande. En faisant ça, le programme de votre choix tournera dans un environnement verrouillé où il ne pourra accéder qu'à ce que vous autorisez explicitement.

Ensuite, vous avez deux philosophies. Soit vous bloquez tout par défaut et vous n'autorisez que le strict nécessaire, c'est-à-dire l'approche parano parfaite pour tester du code louche. Soit vous autorisez tout et vous ne bloquez que ce qui craint. La première est plus sûre, la seconde plus rapide à mettre en place.

Voici un exemple concret pour avoir un terminal coupé du réseau. Suffit de 3 lignes de profil (c'est du LISP) :

(version 1)
(allow default)
(deny network*)

Et là, sandbox-exec -f no-network.sb zsh vous donnera un shell qui peut tout faire sauf se connecter à Internet. Sympa donc pour lancer un script dont vous n'êtes pas sûr à 100% ! Par contre, pour les apps GUI c'est plus capricieux... en testant la même chose avec Firefox, le navigateur arrive quand même à se connecter (il passe probablement par un autre mécanisme réseau). Du coup, pour les applications graphiques, faudra tester au cas par cas.

D'ailleurs, macOS embarque déjà plein de profils dans /System/Library/Sandbox/Profiles/. Ce sont ceux qu'Apple utilise pour ses propres services et certains sont bien commentés, ce qui en fait une super base pour créer les vôtres (Votre IA personnelle en sera ravie ^^).

Côté debug, si un programme plante dans le bac à sable sans explication, la commande log stream --predicate 'sender=="Sandbox"' affichera en temps réel toutes les opérations bloquées. Comme ça, vous voyez exactement ce qui coince et vous ajustez votre profil en conséquence.

Après comme je vous le disais en intro, Apple a officiellement déprécié sandbox-exec car elle préfère pousser son App Sandbox via Xcode, pensé pour les apps du Mac App Store. Mais bon pour isoler rapidement un script en ligne de commande, l'App Sandbox ne sert à rien. Du coup, cet utilitaire CLI reste le seul moyen natif de faire du sandboxing à la volée sur Mac.

Et avec les agents IA qui exécutent du code YOLO partout sur nos machines, avoir un outil comme celui-ci pour isoler un process sans rien installer, c'est plutôt cool je pense ! Si vous utilisez déjà des outils comme Opcode (une GUI pour Claude Code) qui intègrent déjà du sandboxing, c'est exactement cette couche en dessous. Il s'agit de Seatbelt, le framework de sandboxing kernel de macOS, qui fait tout le boulot au niveau OS.

Bref, si la sécurité de votre Mac vous préoccupe, allez gratouiller un peu ça. Tous les profils sont déjà sur votre machine, y'a plus qu'à jouer avec !

Source

Allformusic - Écoutez YouTube écran éteint (et c'est gratuit)

Par : Korben
11 avril 2026 à 09:00

Google déteste que vous écoutiez YouTube gratuitement en arrière-plan. C'est comme ça depuis des années, et ça empire... Par exemple, vous verrouillez l'écran 2 secondes et pouf, plus de son.

YouTube Premium coûte dans les 13 balles par mois pour débloquer ça, ce qui fait quand même plus de 150 euros l'année juste pour écouter de la musique écran éteint. Heureusement, un dev français a trouvé une parade avec Allformusic , un site qui contourne le problème en utilisant l'API officielle de YouTube.

Le principe c'est que Allformusic est une sorte de jukebox géant qui tape directement dans le catalogue YouTube. Vous cherchez un artiste, un album, un genre... et ça lance la musique. Sans compte, sans pub et gratos ! Du coup, si vous voulez vous taper l'intégrale de Daft Punk un dimanche après-midi, vous tapez leur nom et c'est parti.

Et là où c'est malin, c'est que sur mobile, comme YouTube coupe le son dès que vous verrouillez l'écran (merci Google...), y'a une astuce simple à connaitre avec Alloformusic.

Vous lancez votre musique, vous verrouillez le téléphone, le son se coupe... normal. Sauf que le mini-lecteur natif du navigateur apparaît dans vos notifications. Appuyez alors sur Play depuis l'écran de verrouillage, et hop, la musique reprend en arrière-plan !

Par contre, attention si votre navigateur a un mode économie d'énergie agressif, ça peut couper au bout de 10-15 min. Dans ce cas, désactivez l'optimisation batterie pour Chrome dans les paramètres Android.

Le site fonctionne comme une PWA, une Progressive Web App, ce qui veut dire que vous pouvez l'ajouter sur votre smartphone comme une appli classique. Sur Android, direction le menu Chrome puis "Ajouter à l'écran d'accueil". Sur iPhone, c'est via Safari, le bouton partage, puis "Sur l'écran d'accueil".

Bruno, le créateur du site, confirme que ça tourne nickel sur Android. Côté iOS, c'est moins testé... perso, j'aurais tendance à dire que ça dépend de la version de Safari et de comment Apple gère la MediaSession API. À vérifier de votre côté.

D'ailleurs si vous aviez lu mon article sur le blocage de la lecture YouTube en arrière-plan , vous saviez que Google serre la vis sur les navigateurs tiers. Les méthodes à base de about:flags tombent les unes après les autres.

Allformusic a l'avantage de passer par l'API officielle, du coup en théorie c'est moins susceptible de se faire dégager du jour au lendemain... mais bon, avec Google, on n'est jamais à l'abri.

Après, la qualité audio dépend de ce que YouTube balance, et sur certains titres c'est du 128 kbps donc clairement pas du FLAC hi-fi. Si vous êtes du genre à entendre la différence entre un MP3 et un vinyl, passez donc votre chemin. Mais si vous êtes comme et que c'est juste pour se mettre un fond sonore en cuisinant ou dans les transports, ça fait largement le taf.

Y'a pas non plus de mode hors-ligne, donc sans connexion c'est mort. Et vu que ça streame du YouTube, comptez une cinquantaine de Mo par heure en audio seul... à garder en tête si vous avez un forfait serré.

Le site propose aussi des sélections thématiques... Top du moment, Top soirée, et même un "Top Johnny" pour les fans du Taulier ^^. C'est un projet perso fait par un lecteur du blog passionné de code et de musique, donc ça mérite bien un petit coup de projecteur !

Bref, merci à Bruno pour le partage et encore bravo ! Si vous cherchez un moyen d'écouter de la musique en streaming sans prise de tête, allformusic.fr ça vaut le détour.

Dégooglisation - Vos Google Docs ne sont pas de vrais fichiers

Par : Korben
30 mars 2026 à 10:55

Vous savez ce qui est super vicieux avec Google Drive ?

C'est que les fichiers .gdoc, .gsheet et .gslides que vous voyez dans votre explorateur de fichiers et bien ce ne sont pas de VRAIS documents. Ce sont en fait des raccourcis JSON de quelques octets qui pointent vers les serveurs de Google. Essayez par exemple de les copier avec FreeFileSync ... y'a rien dedans ! Vous pensez donc avoir une copie mais le jour où vous fermez votre compte Google, pouf, tout disparaît comme par magie !

Y'a bien sûr Takeout, l'outil officiel d'export de Google, mais cette merde ne résout pas vraiment le problème non plus, car il vous balance un zip géant de tout votre Drive sans garantie de conversion propre des Google Docs en fichiers exploitables. Et l'export manuel via Fichier > Télécharger > Word, quand vous avez des centaines de documents éparpillés dans des dizaines de dossiers... c'est pas trop une option quoi...

Voilà pour notre point petit point d'introduction sur cette situation ubuesque... ^^

Mais heureusement, un lecteur du blog prénommé Jérémy (ça me fait penser à la première saison de Beast Games... Argh !) a pondu Export-GoogleShortcuts , un script PowerShell (Windows, donc) qui règle ça en utilisant l'API officielle Google Drive. Le principe c'est qu'il parcourt vos dossiers, repère les raccourcis .gdoc/.gsheet/.gslides et les convertit automatiquement en .docx, .xlsx et .pptx. Les fichiers atterrissent alors au même endroit que les originaux, avec un suffixe _from_gdocs pour pas mélanger.

Côté mise en place, faut passer par la Google Cloud Console pour créer un petit projet, activer l'API Drive et générer des identifiants OAuth (l'éternel duo clé secrète + ID client). C'est un one-shot, vous le faites une fois et c'est réglé ! Ensuite, une commande PowerShell avec le chemin de votre Drive et vos identifiants, et ça mouline tout seul.

Le truc bien pensé c'est que le script gère les noms de fichiers foireux (caractères spéciaux que Windows digère mal) via une petite recherche mais par contre, attention, y'a une limite ! Hé oui, Google coupera la connexion si le fichier converti dépasse les 10 Mo environ. Donc vos grosses présentations de darons bourrées d'images HD, faudra les télécharger à la main via le navigateur. Après pour le reste, soit environ 95% des fichiers, ça passe sans broncher.

Voilà donc une bonne bidouille pour sortir des griffes de Google... Car oui, stocker ses docs chez Google c'est pratique... mais comme d'hab, le problème, arrive le jour où ça ne l'est plus. Que ce soit pour migrer vers un NAS hébergé chez vous , vers Nextcloud, ou juste pour récupérer une copie locale, ce script fait finalement très bien le boulot.

Et si la conversion de documents entre différents formats c'est votre truc, Pandoc reste un compagnon carrément solide pour tout ce qui est markdown, HTML, ODT ou EPUB.

Voilà, donc n'oubliez pas, vos données vous appartiennent... mais encore faudrait-il les extraire avant que Google ne vous mette une carotte !

Sortie VGA sur un PIC18 : quand l'optimisation hardware devient un art

Par : Korben
26 mars 2026 à 14:11

Générer un signal VGA avec un microcontrôleur 8 bits PIC18 est un défi technique de taille. Ce projet Hackaday montre comment détourner les ressources limitées d'un processeur rudimentaire pour produire une image stable. Une petite plongée dans le bit-banging pur et dur.

Le défi du timing analogique

Le standard VGA impose une rigueur chronométrique absolue à celui qui s'y frotte. Pour obtenir une image stable, typiquement en 640x480 à 60 Hz, le contrôleur doit générer des signaux de synchronisation horizontale (H-sync) et verticale (V-sync) avec une précision de l'ordre de la microseconde. Sur une architecture PIC18 cadencée à quelques dizaines de mégahertz, chaque cycle d'instruction est précieux. L'astuce réside ici dans l'utilisation intelligente des timers internes et des interruptions prioritaires pour maintenir cette cadence sans aucune dérive temporelle, sous peine de voir l'image se désynchroniser immédiatement.

Un DAC rudimentaire pour les couleurs

Côté matériel, la solution retenue est ultra simple (si on peut dire). Pour transformer les sorties numériques binaires du microcontrôleur en signaux analogiques exploitables par un moniteur CRT ou LCD, l'auteur a implémenté une échelle de résistances, aussi appelée DAC R-2R. Ce montage passif permet de convertir des combinaisons de bits en niveaux de tension spécifiques pour les canaux Rouge, Vert et Bleu. C'est une approche classique en électronique "low-cost" qui permet d'obtenir une palette de couleurs certes limitée, mais parfaitement fonctionnelle pour de l'affichage de texte ou de graphismes simples.

L'art du bit-banging et des périphériques détournés

L'envoi des données de pixels vers l'écran nécessite une bande passante que le CPU seul peinerait à fournir en mode pur "bit-banging". Pour optimiser le processus, le développeur détourne souvent le module SPI ou le port série synchrone (MSSP) du PIC pour envoyer les octets de données à la vitesse de l'horloge système. Cela permet de déléguer une partie de la charge de travail au hardware interne et de libérer quelques cycles processeur pour gérer la logique d'affichage. C'est un équilibre précaire où la moindre latence logicielle se traduit par des pixels décalés ou des lignes de travers. Chaud donc.

Ce projet illustre bien l'adage selon lequel la contrainte stimule la créativité. Là où nous utilisons aujourd'hui des processeurs multi-cœurs pour la moindre interface, ce hack prouve qu'un vieux microcontrôleur 8 bits peut encore faire le job. C’est une leçon d'architecture informatique qui permet de comprendre concrètement comment l'information devient image. C'est aussi une forme de résistance face à la démesure logicielle actuelle.

Source : Hackaday

Sora ferme - Comment sauvegarder vos vidéos IA avant la coupure

Par : Korben
25 mars 2026 à 15:22

Sora, c'est fini les amis !

Hé oui, cest chacals d'OpenAI ferment leur plateforme de vidéos IA, et franchement, ça me rend un peu triste. À vrai dire, même si c'était que de la vidéo générée à partir de prompts, moi je me marrais bien. C'était fun de regarder le produit de ses prompts mais aussi de regarder les conneries des autres. Les versions québécoises, aïe aïe aïe, c'était quelque chose quand même !

Mais bon, le plus urgent maintenant, c'est de sauvegarder vos vidéos avant que tout disparaisse. OpenAI n'a pas encore communiqué de date précise pour la coupure, juste un vague « on vous dira bientôt ». Du coup, autant ne pas traîner, parce que quand ce genre de service cloud ferme, en général c'est pas 6 mois de préavis qu'on vous file...

Depuis la fuite du modèle jusqu'à aujourd'hui, Sora aura fait parler de lui. Côté raisons, c'est Fidji Simo (la patronne de la division Applications) qui a lâché le morceau : ils éparpillent leurs efforts sur trop d'apps, d'API et de stacks serveur différents, et ça les ralentit. En gros, entre préparer une entrée en bourse pour fin 2026 et cramer du GPU H100 sur des vidéos de chats en IA, le choix est vite fait. L'équipe de recherche Sora, elle, continuera à bosser sur la simulation de mondes 3D... mais pour la robotique. Et le fameux deal à 1 milliard de dollars avec Disney pour des films et séries ? Pouf, magie magie, c'est envolé !!

Faut dire que les chiffres n'étaient pas glorieux non plus. Après un lancement en fanfare fin 2024 (et une app iOS lancée à l'automne 2025 qui avait cartonné dans les charts), les téléchargements sur l'App Store avaient plongé de 32% entre novembre et décembre 2025. La hype, ça dure qu'un temps.

Mais maintenant les gens, on passe aux choses sérieuses !

Sora Backup - le script qui sauve vos vidéos

Je n'avais absolument pas de temps aujourd'hui, mais j'ai quand même taffé pour vous développer un petit script JavaScript qui récupère TOUTES vos vidéos Sora d'un coup, avec les prompts et les métadonnées, et qui vous génère un joli ZIP prêt à archiver. Pas besoin d'installer quoi que ce soit, pas d'extension louche. Vous avez juste besoin d'être connecté à votre profil Sora et d'un navigateur.

Comment ça marche

Allez sur sora.com , connectez-vous à votre compte, puis ouvrez la console JavaScript de votre navigateur (F12 sur Chrome ou Firefox, onglet Console). Ensuite, glissez-déplacez ou collez le script ci-dessous dedans et appuyez sur Entrée.

Le script va automatiquement récupérer votre token d'authentification (pas besoin de le chercher vous-même), puis il va paginer sur votre profil Sora pour récupérer tous vos posts publiés. Pour chaque post, il extrait les vidéos attachées (MP4), les télécharge, et empaquette le tout dans un fichier ZIP directement dans votre navigateur.

Y'a même un fichier manifest.json dans le ZIP qui contient tous vos prompts, les dimensions, les durées, les permalinks, les dates de création... bref, tout ce qu'il faut pour retrouver vos petits. Le ZIP est généré en format STORE (pas compressé, parce que compresser du MP4 ça sert à rien), avec un calcul CRC32 maison et sans aucune librairie externe.

Le script complet

Voici le code à coller dans la console :

// ==========================================================
// SORA BACKUP - Sauvegarde complète vidéos + images + prompts par Korben
// ==========================================================
// Usage : Ouvrir https://sora.com, F12 > Console, coller ce script
// Les fichiers sont téléchargés via le navigateur (dossier Downloads)
// Un fichier manifest.json récapitule tout (prompts, metadata, URLs)
// ==========================================================

(async () => {
 // --- Mini ZIP builder (STORE, pas de lib externe) ---
 const crc32table = new Uint32Array(256);
 for (let i = 0; i < 256; i++) {
 let c = i;
 for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
 crc32table[i] = c;
 }
 function crc32(buf) {
 let c = 0xFFFFFFFF;
 for (let i = 0; i < buf.length; i++) c = crc32table[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
 return (c ^ 0xFFFFFFFF) >>> 0;
 }
 const zipFiles = []; // {name, data (Uint8Array), crc, size}

 const PAGE_SIZE = 50;
 const DELAY_MS = 1500;
 const manifest = [];
 let totalDownloaded = 0;
 let totalErrors = 0;

 // --- Auth : récupérer le Bearer token ---
 // OPTION 1 : Coller ton token ici (Network tab > Authorization header)
 // OPTION 2 : Laisser vide, le script tentera de le récupérer auto
 let AUTH_TOKEN = '';

 async function getAuthToken() {
 if (AUTH_TOKEN) return AUTH_TOKEN;

 // Auto-detect : endpoint session ChatGPT
 for (const path of ['/api/auth/session', '/backend-api/auth/session']) {
 try {
 const r = await fetch(path, { credentials: 'include' });
 if (r.ok) {
 const json = await r.json();
 if (json.accessToken) {
 AUTH_TOKEN = json.accessToken;
 console.log(' 🔑 Token récupéré automatiquement');
 return AUTH_TOKEN;
 }
 }
 } catch(e) {}
 }

 // Fallback : demander à l'utilisateur
 const input = prompt(
 'Token non trouvé automatiquement.\n\n' +
 'Pour le récupérer :\n' +
 '1. F12 > onglet Réseau\n' +
 '2. Rafraîchis la page\n' +
 '3. Clique sur une requête /backend/...\n' +
 '4. Copie le header Authorization\n\n' +
 'Colle le token ici (Bearer eyJ...):'
 );
 if (input) {
 AUTH_TOKEN = input.replace(/^Bearer\s+/i, '').trim();
 return AUTH_TOKEN;
 }

 console.error(' ❌ Pas de token. Annulation.');
 return null;
 }

 // --- Fetch API avec auth ---
 async function apiFetch(url) {
 const token = await getAuthToken();
 const headers = {};
 if (token) headers['Authorization'] = 'Bearer ' + token;

 // oai-device-id requis par certains endpoints
 const deviceId = localStorage.getItem('oai-did') || '';
 if (deviceId) headers['oai-device-id'] = deviceId;

 const resp = await fetch(url, {
 method: 'GET',
 credentials: 'include',
 headers
 });

 if (!resp.ok) throw new Error(`HTTP ${resp.status} for ${url}`);
 return resp.json();
 }

 // --- Pagination générique ---
 async function fetchAllPages(baseUrl, dataField = 'data', cursorParam = 'after', cursorField = 'last_id') {
 let allItems = [];
 let cursor = '';
 let page = 0;

 while (true) {
 let url = baseUrl;
 if (cursor) url += `&${cursorParam}=${cursor}`;

 console.log(` 📄 Page ${++page} (${allItems.length} items so far)...`);
 const json = await apiFetch(url);

 const items = json[dataField];
 if (!Array.isArray(items) || items.length === 0) break;

 allItems = allItems.concat(items);
 cursor = json[cursorField] || '';

 if (!json.has_more && !cursor) break;
 await sleep(DELAY_MS);
 }

 return allItems;
 }

 // Variante pour les endpoints project_y (cursor-based)
 async function fetchAllPagesCursor(baseUrl) {
 let allItems = [];
 let cursor = '';
 let page = 0;

 while (true) {
 let url = baseUrl;
 if (cursor) url += `&cursor=${cursor}`;

 console.log(` 📄 Page ${++page} (${allItems.length} items so far)...`);
 const json = await apiFetch(url);

 const items = json.items;
 if (!Array.isArray(items) || items.length === 0) break;

 allItems = allItems.concat(items);
 cursor = json.cursor || '';

 if (!cursor) break;
 await sleep(DELAY_MS);
 }

 return allItems;
 }

 function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

 // --- Extraire URL du média depuis une generation ---
 function getMediaUrl(gen) {
 return gen?.encodings?.source?.path
 || gen?.downloadable_url
 || gen?.url
 || '';
 }

 // --- Extraire le prompt (peut être dans actions, prompt, ou input_text) ---
 function getPrompt(item, gen) {
 // Prompt direct
 if (gen?.prompt) return gen.prompt;
 if (item?.prompt) return item.prompt;
 if (item?.input_text) return item.input_text;
 // Storyboard : les actions sont les descriptions des scènes
 if (item?.actions && typeof item.actions === 'object') {
 return Object.entries(item.actions)
 .sort((a,b) => Number(a[0]) - Number(b[0]))
 .map(([frame, desc]) => `[frame ${frame}] ${desc}`)
 .join(' | ');
 }
 if (gen?.actions && typeof gen.actions === 'object') {
 return Object.entries(gen.actions)
 .sort((a,b) => Number(a[0]) - Number(b[0]))
 .map(([frame, desc]) => `[frame ${frame}] ${desc}`)
 .join(' | ');
 }
 return '';
 }

 // --- Dérouler les items du profil Sora en items plats ---
 function flattenProfileItems(items) {
 const flat = [];
 for (const item of items) {
 const post = item.post || item;
 const attachments = post.attachments || [];
 if (attachments.length === 0) continue;

 for (const att of attachments) {
 const url = att.encodings?.source?.path || att.downloadable_url || att.url || '';
 if (!url) continue;

 flat.push({
 id: post.id || att.generation_id || '',
 generation_id: att.generation_id || '',
 task_id: att.task_id || '',
 title: att.title || post.discovery_phrase || '',
 prompt: post.text || '',
 emoji: post.emoji || '',
 type: att.generation_type || att.kind || '',
 width: att.width || 0,
 height: att.height || 0,
 duration_s: att.duration_s || 0,
 is_public: !!post.posted_to_public,
 created_at: post.posted_at ? new Date(post.posted_at * 1000).toISOString() : '',
 url: url,
 permalink: post.permalink || '',
 username: item.profile?.username || '',
 });
 }
 }
 return flat;
 }

 // --- Sanitize filename ---
 function sanitize(name) {
 return name.replace(/[<>:"\/\\|?*\x00-\x1f]/g, '_').substring(0, 100);
 }

 // --- Ajouter un fichier au ZIP ---
 async function addToZip(url, filename) {
 try {
 const resp = await fetch(url);
 if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
 const buf = await resp.arrayBuffer();
 const data = new Uint8Array(buf);
 zipFiles.push({ name: filename, data, crc: crc32(data), size: data.length });
 totalDownloaded++;
 return true;
 } catch(e) {
 console.warn(` ⚠️ Erreur ${filename}:`, e.message);
 totalErrors++;
 return false;
 }
 }

 // --- Déduire l'extension ---
 function getExt(url, type) {
 if (!url) return type === 'video' ? '.mp4' : '.png';
 const m = url.match(/\.(mp4|webm|mov|png|jpg|jpeg|webp|gif)/i);
 return m ? '.' + m[1].toLowerCase() : (type === 'video' ? '.mp4' : '.png');
 }

 // ==========================================================
 // MAIN
 // ==========================================================
 const origin = window.location.origin;
 console.log('🎬 SORA BACKUP - Démarrage');
 console.log('='.repeat(50));

 // 1. Mes posts Sora (profil)
 console.log('\n📦 1/2 - Récupération de mes posts Sora...');
 let myPosts = [];
 try {
 myPosts = await fetchAllPagesCursor(
 `${origin}/backend/project_y/profile_feed/me?limit=${PAGE_SIZE}&cut=nf2`
 );
 console.log(`  ${myPosts.length} posts de profil`);
 // Debug premier item
 if (myPosts.length > 0) {
 const first = myPosts[0];
 console.log(' 🔍 Premier item - clés:', Object.keys(first).join(', '));
 console.log(' 🔍 URL:', first.url?.substring(0, 80) || 'none');
 console.log(' 🔍 DL:', first.downloadable_url?.substring(0, 80) || 'none');
 console.log(' 🔍 ENC:', first.encodings?.source?.path?.substring(0, 80) || 'none');
 console.log(' 🔍 GENS:', first.generations?.length || 'none');
 console.log(' 🔍 TITLE:', first.title || 'none');
 }
 } catch(e) {
 console.warn(' ⚠️ profil failed:', e.message);
 }

 // 2. Mes likes sur Sora
 console.log('\n📦 2/2 - Récupération de mes likes Sora...');
 let myLikes = [];
 try {
 myLikes = await fetchAllPagesCursor(
 `${origin}/backend/project_y/profile_feed/me?limit=${PAGE_SIZE}&cut=appearances`
 );
 if (myCameos.length) console.log(`  ${myCameos.length} cameos trouvés`);
 } catch(e) {}

 // --- Dérouler les generations et dédupliquer ---
 console.log('\n🔄 Extraction des vidéos...');
 const rawAll = [...myPosts, ...myLikes];
 const flatItems = flattenProfileItems(rawAll);

 const seen = new Set();
 const allItems = [];
 for (const item of flatItems) {
 if (item.id && seen.has(item.id)) continue;
 // Filtrer : vidéos uniquement
 const isVideo = item.type === 'video_gen' || item.url.includes('/videos/') || item.url.includes('.mp4');
 if (!isVideo) continue;
 if (item.id) seen.add(item.id);
 allItems.push(item);
 }

 console.log(`📊 Total unique: ${allItems.length} vidéos à télécharger`);
 console.log('='.repeat(50));

 // --- Construire le manifest et télécharger ---
 console.log('\n⬇️ Téléchargement en cours...');
 console.log('(Les fichiers arrivent dans ton dossier Downloads)');

 for (let i = 0; i < allItems.length; i++) {
 const meta = allItems[i];
 const url = meta.url;

 if (!url) {
 console.log(` ⏭️ [${i+1}/${allItems.length}] ${meta.id} - pas d'URL, skip`);
 meta.downloaded = false;
 manifest.push(meta);
 continue;
 }

 const type = (meta.task_type === 'image_gen' || url.match(/\.(png|jpg|jpeg|webp|gif)/i)) ? 'image' : 'video';
 const ext = getExt(url, type);
 const nameBase = meta.title
 ? sanitize(meta.title)
 : (meta.prompt ? sanitize(meta.prompt.substring(0, 60)) : meta.id);
 const filename = `sora_${String(i+1).padStart(4,'0')}_${nameBase}${ext}`;

 console.log(` ⬇️ [${i+1}/${allItems.length}] ${filename}`);
 meta.filename = filename;
 meta.downloaded = await addToZip(url, filename);
 manifest.push(meta);

 // Pause entre downloads pour pas surcharger
 if (i < allItems.length - 1) await sleep(800);
 }

 // --- Ajouter le manifest au ZIP ---
 console.log('\n📝 Ajout du manifest au ZIP...');
 const manifestData = new TextEncoder().encode(JSON.stringify(manifest, null, 2));
 zipFiles.push({ name: 'manifest.json', data: manifestData, crc: crc32(manifestData), size: manifestData.length });

 // --- Générer le ZIP (format STORE, pas de compression) ---
 console.log('\n📦 Génération du ZIP...');
 const enc = new TextEncoder();
 const blobParts = [];
 const centralParts = [];
 let offset = 0;

 for (const f of zipFiles) {
 const nameBytes = enc.encode(f.name);
 // Local file header (30 bytes + name)
 const lh = new ArrayBuffer(30);
 const lv = new DataView(lh);
 lv.setUint32(0, 0x04034b50, true);
 lv.setUint16(4, 20, true);
 lv.setUint16(8, 0, true); // STORE
 lv.setUint32(14, f.crc, true);
 lv.setUint32(18, f.size, true);
 lv.setUint32(22, f.size, true);
 lv.setUint16(26, nameBytes.length, true);
 blobParts.push(new Uint8Array(lh), nameBytes, f.data);

 // Central directory entry (46 bytes + name)
 const ch = new ArrayBuffer(46);
 const cv = new DataView(ch);
 cv.setUint32(0, 0x02014b50, true);
 cv.setUint16(4, 20, true);
 cv.setUint16(6, 20, true);
 cv.setUint16(10, 0, true); // STORE
 cv.setUint32(16, f.crc, true);
 cv.setUint32(20, f.size, true);
 cv.setUint32(24, f.size, true);
 cv.setUint16(28, nameBytes.length, true);
 cv.setUint32(42, offset, true);
 centralParts.push(new Uint8Array(ch), nameBytes);

 offset += 30 + nameBytes.length + f.size;
 }

 const centralSize = centralParts.reduce((s, p) => s + p.length, 0);
 const eocd = new ArrayBuffer(22);
 const ev = new DataView(eocd);
 ev.setUint32(0, 0x06054b50, true);
 ev.setUint16(8, zipFiles.length, true);
 ev.setUint16(10, zipFiles.length, true);
 ev.setUint32(12, centralSize, true);
 ev.setUint32(16, offset, true);

 const zipBlob = new Blob([...blobParts, ...centralParts, new Uint8Array(eocd)], { type: 'application/zip' });

 const zipName = `sora_backup_${new Date().toISOString().split('T')[0]}.zip`;
 const a = document.createElement('a');
 a.href = URL.createObjectURL(zipBlob);
 a.download = zipName;
 document.body.appendChild(a);
 a.click();
 document.body.removeChild(a);
 URL.revokeObjectURL(a.href);

 // --- Résumé ---
 const sizeMB = (zipBlob.size / 1024 / 1024).toFixed(1);
 console.log('\n' + '='.repeat(50));
 console.log('🎬 SORA BACKUP TERMINÉ');
 console.log(`  Vidéos dans le ZIP : ${totalDownloaded}`);
 console.log(`  Erreurs : ${totalErrors}`);
 console.log(` 📦 Fichier : ${zipName} (${sizeMB} MB)`);
 console.log(` 📝 manifest.json inclus dans le ZIP`);
 console.log('='.repeat(50));
})();

Quelques précisions

Si le token n'est pas récupéré automatiquement (ça peut arriver selon votre config), le script vous demandera de le coller manuellement. Pour le trouver, c'est simple : F12 > onglet Réseau > rafraîchissez la page > cliquez sur n'importe quelle requête vers /backend/... > copiez le header Authorization.

D'ailleurs, si la vidéo IA vous branche toujours, Higgsfield propose des séries entièrement générées par IA. C'est pas la même approche que Sora, mais c'est un signe que la vidéo IA ne meurt pas avec la fermeture d'un seul service.

Bon, bref, c'est la fin d'un truc sympa. Moi je préférais largement scroller sur Sora que d'aller sur TikTok ou Instagram parce qu'au moins c'était drôle !

Merci à mes Patreons qui me permettent de prendre le temps de développer ce genre de petits outils pour vous. Sans eux, j'aurais jamais pu me poser une après-midi pour coder ça.

Source

Un développeur fait tourner du code Arduino sur une puce de 1980

Par : Korben
24 mars 2026 à 10:24

Un développeur vietnamien a trouvé le moyen de faire fonctionner du code Arduino sur un microcontrôleur 8051, une architecture conçue par Intel en 1980.

L'astuce repose sur un émulateur RISC-V intégré directement dans la puce, et le tout est disponible en open source sur GitHub.

Une puce de 45 ans qui refuse de mourir

Le 8051, c'est un microcontrôleur 8 bits qu'Intel a conçu en 1980. L'anecdote veut que son architecture ait été dessinée en un week-end par l'ingénieur John Wharton.

Depuis, Intel a vendu plus de 100 millions d'unités rien que sur la première décennie, et des variantes compatibles sont encore produites et utilisées un peu partout, des souris d'ordinateur aux puces Bluetooth.

La version ciblée ici, c'est le STC8H8K64U, un dérivé moderne fabriqué par le chinois STC Micro. Il coûte moins d'un dollar et reste populaire en Asie, mais les outils de développement modernes ne le prennent pas en charge. D'où l'idée du projet.

Un émulateur RISC-V dans un 8051

Bùi Trịnh Thế Viên n'a pas cherché à porter le compilateur Arduino directement sur l'architecture 8051, ce qui aurait été un chantier monstre.

Il a opté pour une approche détournée : intégrer un émulateur RISC-V (appelé rv51, écrit en assembleur 8051 par un autre développeur, cyrozap) dans la puce STC8. Le code Arduino est compilé pour RISC-V, puis exécuté via cet émulateur.

Le projet est disponible sur GitHub sous le nom STC_Arduino_Core.

Des limites assumées

L'émulation a un coût. L'émulateur consomme 8 Ko de mémoire flash sur la puce, et la vitesse d'exécution est divisée par 100 à 1 000 par rapport au code natif. Pour le code qui demande du temps réel, comme la gestion des interruptions, il faut repasser sur de l'assembleur 8051 classique.

Et puis il faut le dire, des microcontrôleurs RISC-V natifs existent et coûtent à peine plus cher. Le projet reste donc un exercice technique et pédagogique, pas une solution de production.

C'est le genre de bidouille qui fait sourire. Faire tourner du code Arduino sur une architecture de 1980 via un émulateur RISC-V coincé dans 8 Ko, il fallait quand même y penser.

Bon par contre, on ne va pas se raconter d'histoires, en pratique ça n'a pas beaucoup d'intérêt face à un vrai microcontrôleur RISC-V à 2 euros. Mais l'exercice a le mérite de prouver que le 8051 a encore de la ressource, 45 ans après sa création.

Source : Hackaday

❌
❌