twitter: add option to convert .mp4 to .gif

This commit is contained in:
wukko 2024-01-17 11:38:51 +06:00
parent 67329199e8
commit a63a35c74d
10 changed files with 81 additions and 12 deletions

View file

@ -90,7 +90,8 @@
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"], "mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
"copy": ["-c:a", "copy"], "copy": ["-c:a", "copy"],
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"], "audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
"m4a": ["-movflags", "frag_keyframe+empty_moov"] "m4a": ["-movflags", "frag_keyframe+empty_moov"],
"gif": ["-vf", "scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", "-loop", "0"]
}, },
"sponsors": [{ "sponsors": [{
"name": "royale", "name": "royale",

View file

@ -30,6 +30,7 @@ const checkboxes = [
"reduceTransparency", "reduceTransparency",
"disableAnimations", "disableAnimations",
"disableMetadata", "disableMetadata",
"twitterGif",
]; ];
const exceptions = { // used for mobile devices const exceptions = { // used for mobile devices
"vQuality": "720" "vQuality": "720"
@ -381,6 +382,7 @@ async function download(url) {
} }
if (sGet("disableMetadata") === "true") req.disableMetadata = true; if (sGet("disableMetadata") === "true") req.disableMetadata = true;
if (sGet("twitterGif") === "true") req.twitterGif = true;
let j = await fetch(`${apiURL}/api/json`, { let j = await fetch(`${apiURL}/api/json`, {
method: "POST", method: "POST",

View file

@ -158,6 +158,8 @@
"UrgentTwitterPatch": "fixes and easier downloads", "UrgentTwitterPatch": "fixes and easier downloads",
"StatusPage": "service status page", "StatusPage": "service status page",
"TroubleshootingGuide": "self-troubleshooting guide", "TroubleshootingGuide": "self-troubleshooting guide",
"UpdateNewYears": "new years clean up" "UpdateNewYears": "new years clean up",
"SettingsTwitterGif": "convert gifs to .gif",
"SettingsTwitterGifDescription": ".gif is lossy and extremely inefficient. file sizes may be larger than expected. use only when necessary."
} }
} }

View file

@ -327,6 +327,16 @@ export default function(obj) {
padding: "no-margin" padding: "no-margin"
}]) }])
}) })
+ settingsCategory({
name: "twitter",
title: "twitter",
body: checkbox([{
action: "twitterGif",
name: t("SettingsTwitterGif"),
padding: "no-margin"
}])
+ explanation(t('SettingsTwitterGifDescription'))
})
+ settingsCategory({ + settingsCategory({
name: "codec", name: "codec",
title: t('SettingsCodecSubtitle'), title: t('SettingsCodecSubtitle'),

View file

@ -37,7 +37,8 @@ export default async function(host, patternMatch, url, lang, obj) {
case "twitter": case "twitter":
r = await twitter({ r = await twitter({
id: patternMatch.id, id: patternMatch.id,
index: patternMatch.index - 1 index: patternMatch.index - 1,
toGif: obj.twitterGif
}); });
break; break;
case "vk": case "vk":
@ -166,7 +167,11 @@ export default async function(host, patternMatch, url, lang, obj) {
: loc(lang, r.error) : loc(lang, r.error)
}) })
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, obj.filenamePattern) return matchActionDecider(
r, host, obj.aFormat, isAudioOnly,
lang, isAudioMuted, disableMetadata,
obj.filenamePattern, obj.twitterGif
)
} catch (e) { } catch (e) {
return apiJSON(0, { t: genericError(lang, host) }) return apiJSON(0, { t: genericError(lang, host) })
} }

View file

@ -3,7 +3,7 @@ import { apiJSON } from "../sub/utils.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js"; import createFilename from "./createFilename.js";
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) { export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif) {
let action, let action,
responseType = 2, responseType = 2,
defaultParams = { defaultParams = {
@ -14,13 +14,14 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
fileMetadata: !disableMetadata ? r.fileMetadata : false fileMetadata: !disableMetadata ? r.fileMetadata : false
}, },
params = {}, params = {},
audioFormat = String(userFormat) audioFormat = String(userFormat);
if (r.isPhoto) action = "photo"; if (r.isPhoto) action = "photo";
else if (r.picker) action = "picker" else if (r.picker) action = "picker"
else if (isAudioMuted) action = "muteVideo"; else if (isAudioMuted) action = "muteVideo";
else if (isAudioOnly) action = "audio"; else if (isAudioOnly) action = "audio";
else if (r.isM3U8) action = "singleM3U8"; else if (r.isM3U8) action = "singleM3U8";
else if (r.isGif && toGif) action = "gif";
else action = "video"; else action = "video";
if (action === "picker" || action === "audio") { if (action === "picker" || action === "audio") {
@ -40,6 +41,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
responseType = 1; responseType = 1;
break; break;
case "gif":
params = { type: "gif" }
break;
case "singleM3U8": case "singleM3U8":
params = { type: "remux" } params = { type: "remux" }
break; break;

View file

@ -72,7 +72,7 @@ const requestTweet = (tweetId, token) => {
}) })
} }
export default async function({ id, index }) { export default async function({ id, index, toGif }) {
let guestToken = await getGuestToken(); let guestToken = await getGuestToken();
if (!guestToken) return { error: 'ErrorCouldntFetch' }; if (!guestToken) return { error: 'ErrorCouldntFetch' };
@ -110,7 +110,8 @@ export default async function({ id, index }) {
type: needsFixing(media[0]) ? "remux" : "normal", type: needsFixing(media[0]) ? "remux" : "normal",
urls: bestQuality(media[0].video_info.variants), urls: bestQuality(media[0].video_info.variants),
filename: `twitter_${id}.mp4`, filename: `twitter_${id}.mp4`,
audioFilename: `twitter_${id}_audio` audioFilename: `twitter_${id}_audio`,
isGif: media[0].type === "animated_gif"
}; };
default: default:
const picker = media.map((video, i) => { const picker = media.map((video, i) => {
@ -120,7 +121,9 @@ export default async function({ id, index }) {
service: 'twitter', service: 'twitter',
type: 'remux', type: 'remux',
u: url, u: url,
filename: `twitter_${id}_${i + 1}.mp4` filename: `twitter_${id}_${i + 1}.mp4`,
isGif: media[0].type === "animated_gif",
toGif: toGif ?? false
}) })
} }
return { return {

View file

@ -1,4 +1,4 @@
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js"; import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js";
export default async function(res, streamInfo) { export default async function(res, streamInfo) {
try { try {
@ -10,6 +10,9 @@ export default async function(res, streamInfo) {
case "render": case "render":
await streamLiveRender(streamInfo, res); await streamLiveRender(streamInfo, res);
break; break;
case "gif":
convertToGif(streamInfo, res);
break;
case "remux": case "remux":
case "mute": case "mute":
streamVideoOnly(streamInfo, res); streamVideoOnly(streamInfo, res);

View file

@ -212,3 +212,40 @@ export function streamVideoOnly(streamInfo, res) {
shutdown(); shutdown();
} }
} }
export function convertToGif(streamInfo, res) {
let process;
const shutdown = () => (killProcess(process), closeResponse(res));
try {
let args = [
'-loglevel', '-8'
]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push('-i', streamInfo.urls)
args = args.concat(ffmpegArgs["gif"]);
args.push('-f', "gif", 'pipe:3');
process = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
const [,,, muxOutput] = process.stdio;
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename.split('.')[0] + ".gif"));
pipe(muxOutput, res, shutdown);
process.on('close', shutdown);
res.on('finish', shutdown);
} catch {
shutdown();
}
}

View file

@ -8,7 +8,7 @@ const apiVar = {
aFormat: ["best", "mp3", "ogg", "wav", "opus"], aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"] filenamePattern: ["classic", "pretty", "basic", "nerdy"]
}, },
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"] booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
} }
const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '==']; const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
@ -84,7 +84,8 @@ export function checkJSONPost(obj) {
isAudioMuted: false, isAudioMuted: false,
disableMetadata: false, disableMetadata: false,
dubLang: false, dubLang: false,
vimeoDash: false vimeoDash: false,
twitterGif: false
} }
try { try {
let objKeys = Object.keys(obj); let objKeys = Object.keys(obj);