twitter: add option to convert .mp4 to .gif
This commit is contained in:
parent
67329199e8
commit
a63a35c74d
10 changed files with 81 additions and 12 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") {
|
||||||
|
@ -39,6 +40,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
||||||
case "photo":
|
case "photo":
|
||||||
responseType = 1;
|
responseType = 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "gif":
|
||||||
|
params = { type: "gif" }
|
||||||
|
break;
|
||||||
|
|
||||||
case "singleM3U8":
|
case "singleM3U8":
|
||||||
params = { type: "remux" }
|
params = { type: "remux" }
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue