mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
Vertical Shorts were played in a player forced to 100%x100% on a landscape frame, so they looked wrong (pillarboxed/small). Option A: detect vertical at ingest, persist it, and have every player honor it. - Ingest (routes/content.js): detect a Short from the /shorts/ URL form OR portrait oEmbed dims (oEmbed now queried with the ORIGINAL url so /shorts/ reports its true dimensions), and persist it as st_aspect=vertical on the stored embed URL. That's the only signal players get (remote_url), so it must be captured at ingest, not re-derived per loop. YouTube ignores the unknown param; players read the video id, not the full URL, to build the embed. - Players read st_aspect=vertical and center a 9:16 box (fills a portrait screen, pillarboxes cleanly on landscape) instead of 100%x100%: web (player/index.html), Android (WebViewSupport.youtubeEmbedHtml), Tizen (player.js single-zone + zone paths). Dashboard library uses a static thumbnail, so it's unaffected. Not doing Option B (yt-dlp): runtime dep + storage/bandwidth + maintenance + YouTube ToS; embed-disabled Shorts already skip gracefully. Tests: youtube-shorts.test.js (4) — /shorts/ and portrait-dims tag vertical, landscape stays untagged, /shorts/ tags even if oEmbed fails. Android compiles; web player inline JS + Tizen player.js parse. Note: pre-existing Shorts added before this aren't retagged (would need an oEmbed backfill) — re-add to fix, or a follow-up migration. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.2 KiB
JavaScript
71 lines
3.2 KiB
JavaScript
'use strict';
|
|
|
|
// #184 — YouTube Shorts aspect. Verifies the ingest side of Option A: a Short is
|
|
// detected (from the /shorts/ URL form OR portrait oEmbed dims) and persisted as
|
|
// st_aspect=vertical on the stored embed URL, which every player reads to render
|
|
// 9:16. A normal landscape video is left untagged.
|
|
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const crypto = require('node:crypto');
|
|
process.env.DATA_DIR = path.join(os.tmpdir(), 'st-yt-' + crypto.randomBytes(4).toString('hex'));
|
|
process.env.SELF_HOSTED = 'true';
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
const { test, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const express = require('express');
|
|
const { db } = require('../db/database');
|
|
const contentRouter = require('../routes/content');
|
|
|
|
db.pragma('foreign_keys = OFF'); // skip user/workspace FK setup for this unit
|
|
|
|
// Mock oEmbed. Capture the real fetch first so test HTTP calls still hit the server.
|
|
let oembedResponse = { title: 'Vid', height: 270, width: 480 };
|
|
let oembedOk = true;
|
|
const realFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => ({ ok: oembedOk, json: async () => oembedResponse });
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use((req, _res, next) => { req.user = { id: 'u1' }; req.workspaceId = 'ws1'; next(); });
|
|
app.use('/api/content', contentRouter);
|
|
const server = app.listen(0);
|
|
let base;
|
|
before(async () => { await new Promise(r => server.listening ? r() : server.once('listening', r)); base = `http://127.0.0.1:${server.address().port}`; });
|
|
after(() => { server.close(); globalThis.fetch = realFetch; });
|
|
|
|
const addYoutube = async (url) => {
|
|
const r = await realFetch(`${base}/api/content/youtube`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }),
|
|
});
|
|
return { status: r.status, body: await r.json() };
|
|
};
|
|
|
|
test('/shorts/ URL is tagged st_aspect=vertical', async () => {
|
|
oembedResponse = { title: 'A Short', height: 200, width: 113 };
|
|
const { status, body } = await addYoutube('https://www.youtube.com/shorts/3HiJYYIupd4');
|
|
assert.equal(status, 201);
|
|
assert.ok(/\/embed\/3HiJYYIupd4\?/.test(body.remote_url), 'embed URL built with the extracted id');
|
|
assert.ok(/st_aspect=vertical/.test(body.remote_url), 'Short tagged vertical');
|
|
});
|
|
|
|
test('watch URL with portrait oEmbed dims is tagged vertical', async () => {
|
|
oembedResponse = { title: 'Vertical watch', height: 1920, width: 1080 };
|
|
const { body } = await addYoutube('https://www.youtube.com/watch?v=3HiJYYIupd4');
|
|
assert.ok(/st_aspect=vertical/.test(body.remote_url), 'portrait dims -> vertical');
|
|
});
|
|
|
|
test('normal landscape watch URL is NOT tagged', async () => {
|
|
oembedResponse = { title: 'Normal', height: 270, width: 480 };
|
|
const { body } = await addYoutube('https://www.youtube.com/watch?v=abcdefghijk');
|
|
assert.ok(!/st_aspect=vertical/.test(body.remote_url), 'landscape stays untagged');
|
|
});
|
|
|
|
test('/shorts/ URL is tagged even when oEmbed fails', async () => {
|
|
oembedOk = false;
|
|
const { body } = await addYoutube('https://www.youtube.com/shorts/zzzzzzzzzzz');
|
|
assert.ok(/st_aspect=vertical/.test(body.remote_url), 'the /shorts/ form alone is sufficient');
|
|
oembedOk = true;
|
|
});
|