Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video quality selection #5

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ help: |
/help — this message
/language — change language
/audio — convert all files to audio
/max_quality - automatically download videos in maximum quality
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вместо этого, давай сделаем /chooseResolution, который по дефолту false


Questions, feedback 👉 @borodutch_support.
language: 😶 Please, select the language.
language_selected: 😎 I speak English now.
audio_on: 📣 I will convert all files to audio-only automatically.
audio_off: 🎥 I will not convert all files to audio-only automatically.
max_quality_on: 🎬 I will download all videos in maximum quality.
max_quality_off: 🎬 I'll let you choose the video quality.
# User-facing progress messages
download_started: 😍 Downloading...
uploading_started: 🤘 Uploading...
download_complete: 🎉 Download complete!
check_resolutions: ⏳ Search for available resolutions...
# Messages
resolutions_question: 'Select resolution:'
# Caption for completed video
video_caption: |
<b>${title}</b>
Expand All @@ -28,3 +34,4 @@ error_cache_or_download_job: 😱 I couldn't get the video from cache or create
error_video_download: 😱 I couldn't download the video.
error_video_upload: 😱 I couldn't upload the video.
error_reboot: 😱 The server rebooted, while I was working on the file. Please, try again!
error_outdated_menu: 😱 Sorry, this menu is outdated
7 changes: 7 additions & 0 deletions locales/ru.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ help_ru: |
/help — это сообщение
/language — выбрать язык
/audio — преобразовывать все файлы в аудио
/max_quality - автоматически скачивать видео в максимальном качестве

Вопросы, обратная связь 👉 @borodutch_support.
language: 😶 Пожалуйста, выберите язык.
language_selected: 😎 Теперь я говорю по-русски.
audio_on: 📣 Я буду преобразовывать все файлы в аудио автоматически.
audio_off: 🎥 Я не буду преобразовывать все файлы в аудио автоматически.
max_quality_on: 🎬 Я буду скачивать все видео в максимальном качетсве.
max_quality_off: 🎬 Я дам тебе выбрать качество при скачивании.
# User-facing progress messages
download_started: 😍 Качаю...
uploading_started: 🤘 Загружаю...
download_complete: 🎉 Загрузка завершена!
check_resolutions: ⏳ Определение доступных разрешений...
# Messages
resolutions_question: 'Выберите разрешение:'
# Caption for completed video
video_caption: |
<b>${title}</b>
Expand All @@ -28,3 +34,4 @@ error_cache_or_download_job: 😱 Я не смог найти видео в ке
error_video_download: 😱 Я не смог скачать видео.
error_video_upload: 😱 Я не смог загрузить видео.
error_reboot: 😱 Сервер перезагрузился, пока я работал над файлом. Пожалуйста, попробуйте еще раз!
error_outdated_menu: 😱 Извините, это меню устарело
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@borodutch-labs/yt-dlp-exec": "2.0.0",
"@grammyjs/i18n": "^0.3.0",
"@grammyjs/menu": "^1.0.4",
"@grammyjs/runner": "^1.0.2",
"@typegoose/typegoose": "^9.2.0",
"envalid": "^7.2.2",
Expand Down
Binary file added src/.DS_Store
Binary file not shown.
5 changes: 5 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import bot from '@/helpers/bot'
import cleanupDownloadJobs from '@/helpers/cleanupDownloadJobs'
import configureI18n from '@/middlewares/configureI18n'
import handleAudio from '@/handlers/handleAudio'
import handleMaxQuality from '@/handlers/handleMaxQuality'
import handleUrl from '@/handlers/handleUrl'
import i18n from '@/helpers/i18n'
import ignoreOldMessageUpdates from '@/middlewares/ignoreOldMessageUpdates'
import report from '@/helpers/report'
import sendHelp from '@/handlers/sendHelp'
import startMongo from '@/helpers/startMongo'
import { resolutionMenu } from './menus/resolutionMenu'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не используй относительные импорты, только @


async function runApp() {
console.log('Starting app...')
Expand All @@ -33,6 +35,9 @@ async function runApp() {
bot.command(['help', 'start'], sendHelp)
bot.command('language', sendLanguage)
bot.command('audio', handleAudio)
bot.command('max_quality', handleMaxQuality)
// Menus
bot.use(resolutionMenu)
// Handlers
bot.hears(
/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/i,
Expand Down
14 changes: 14 additions & 0 deletions src/handlers/handleMaxQuality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Context from '@/models/Context'

export default async function handleMaxQuality(ctx: Context) {
ctx.dbchat.autoMaxQuality = !ctx.dbchat.autoMaxQuality
await ctx.dbchat.save()
return ctx.reply(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use replyWithLocalization

ctx.i18n.t(
ctx.dbchat.autoMaxQuality ? 'max_quality_on' : 'max_quality_off'
),
{
reply_to_message_id: ctx.message?.message_id,
}
)
}
21 changes: 19 additions & 2 deletions src/handlers/handleUrl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Context from '@/models/Context'
import createDownloadJobAndRequest from '@/helpers/createDownloadJobAndRequest'
import report from '@/helpers/report'
import { resolutionMenu } from '@/menus/resolutionMenu'
import bot from '@/helpers/bot'
import { findOrCreateShortUrl } from '@/models/ShortUrl'

export default function handleUrl(ctx: Context) {
export default async function handleUrl(ctx: Context) {
try {
const match = ctx.message?.text?.match(
/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/i
Expand All @@ -12,8 +15,22 @@ export default function handleUrl(ctx: Context) {
reply_to_message_id: ctx.message?.message_id,
})
}

const url = match[0]
return createDownloadJobAndRequest(ctx, url)

if (ctx.dbchat.autoMaxQuality) {
return createDownloadJobAndRequest(ctx, url)
} else {
const waitMessage = await ctx.reply(ctx.i18n.t('check_resolutions'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use replyWithLocalization

const shortUrl = await findOrCreateShortUrl(url)
bot.api.deleteMessage(waitMessage.chat.id, waitMessage.message_id)

ctx.shortUrlId = shortUrl.shortId
return ctx.reply(ctx.i18n.t('resolutions_question'), {
reply_markup: resolutionMenu,
reply_to_message_id: ctx.message?.message_id,
})
}
} catch (error) {
report(error, { ctx, location: 'handleUrl' })
return ctx.reply(ctx.i18n.t('error_cannot_start_download'), {
Expand Down
19 changes: 19 additions & 0 deletions src/helpers/checkAvailableResolutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import report from './report'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const youtubedl = require('@borodutch-labs/yt-dlp-exec')

export default async function checkAvailableResolutions(url: string) {
try {
const json = await youtubedl(url, { dumpJson: true })

return Array.from(
new Set<number>(
json.formats
.map((f: any) => f.height)
.filter((height: any) => height >= 144)
)
)
} catch (error) {
report(error, { location: 'checkResolutions', meta: url })
}
}
13 changes: 10 additions & 3 deletions src/helpers/checkForCachedUrlAndSendFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@ import sendCompletedFile from '@/helpers/sendCompletedFile'
export default async function checkForCachedUrlAndSendFile(
url: string,
ctx: Context,
editor: MessageEditor
editor: MessageEditor,
resolution?: number
) {
const cachedUrl = await findUrl(url, ctx.dbchat.audio)
const cachedUrl = await findUrl(url, ctx.dbchat.audio, resolution)

if (cachedUrl) {
console.log(`Sending cached file for ${url}`)

const replyTo = resolution
? ctx.callbackQuery?.message?.reply_to_message?.message_id
: ctx.msg?.message_id

if (editor.messageId) {
await editor.editMessage(ctx.i18n.t('download_complete'))
}
if (ctx.msg) {
return sendCompletedFile(
ctx.dbchat.telegramId,
ctx.msg?.message_id,
replyTo!,
ctx.dbchat.language,
ctx.dbchat.audio,
cachedUrl.title,
Expand Down
9 changes: 6 additions & 3 deletions src/helpers/createDownloadJobAndRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import report from '@/helpers/report'

export default async function createDownloadJobAndRequest(
ctx: Context,
url: string
url: string,
resolution?: number
) {
// Create message editor
const downloadMessageEditor = new MessageEditor(undefined, ctx)
Expand All @@ -20,7 +21,8 @@ export default async function createDownloadJobAndRequest(
const cached = await checkForCachedUrlAndSendFile(
url,
ctx,
downloadMessageEditor
downloadMessageEditor,
resolution
)
if (cached) {
return
Expand All @@ -41,7 +43,8 @@ export default async function createDownloadJobAndRequest(
url,
ctx.dbchat.audio,
ctx.dbchat.telegramId,
message_id
message_id,
resolution
)
// Create download request
await findOrCreateDownloadRequest(
Expand Down
10 changes: 8 additions & 2 deletions src/helpers/downloadUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export default async function downloadUrl(
try {
console.log(`Downloading url ${downloadJob.url}`)
// Download

const videoFormatString = downloadJob.resolution
? `[height=${downloadJob.resolution}][filesize<=?2G][ext=mp4]/[height=${downloadJob.resolution}][filesize<=?2G]`
: '[filesize<=?2G]'

const config = {
dumpSingleJson: true,
noWarnings: true,
Expand All @@ -34,7 +39,7 @@ export default async function downloadUrl(
noPlaylist: true,
format: downloadJob.audio
? 'bestaudio[filesize<=?2G]'
: '[filesize<=?2G]',
: videoFormatString,
maxFilesize: '2048m',
noCallHome: true,
noProgress: true,
Expand Down Expand Up @@ -82,7 +87,8 @@ export default async function downloadUrl(
downloadJob.url,
fileId,
downloadJob.audio,
escapedTitle || 'No title'
escapedTitle || 'No title',
downloadJob.resolution
)
downloadJob.status = DownloadJobStatus.finished
await downloadJob.save()
Expand Down
29 changes: 29 additions & 0 deletions src/menus/resolutionMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import createDownloadJobAndRequest from '@/helpers/createDownloadJobAndRequest'
import Context from '@/models/Context'
import { findShortUrl } from '@/models/ShortUrl'
import { Menu } from '@grammyjs/menu'

export const resolutionMenu = new Menu<Context>('resolution-menu').dynamic(
async (ctx, range) => {
const shortUrl = await findShortUrl(
ctx.shortUrlId || ctx.match?.toString()!
)

if (!shortUrl) {
ctx.reply(ctx.i18n.t('error_outdated_menu'))
return
}

for (const resolution of shortUrl.availableResolutions) {
range
.text({ text: `${resolution}p`, payload: shortUrl.shortId }, (ctx) => {
createDownloadJobAndRequest(ctx, shortUrl.url, resolution)
return ctx.deleteMessage()
})
.row()
}
range.text({ text: `Cancel`, payload: shortUrl.shortId }, (ctx) =>
ctx.deleteMessage()
)
}
)
2 changes: 2 additions & 0 deletions src/models/Chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export class Chat extends FindOrCreate {
language!: string
@prop({ required: true, default: false })
audio!: boolean
@prop({ required: true, default: false })
autoMaxQuality!: boolean
}

const ChatModel = getModelForClass(Chat, {
Expand Down
1 change: 1 addition & 0 deletions src/models/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { I18nContext } from '@grammyjs/i18n/dist/source'
interface Context extends BaseContext {
readonly i18n: I18nContext
dbchat: DocumentType<Chat>
shortUrlId: string
}

export default Context
2 changes: 2 additions & 0 deletions src/models/DownloadJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export default class DownloadJob extends FindOrCreate {
originalChatId!: number
@prop({ required: true, index: true })
originalMessageId!: number
@prop({ index: true })
resolution?: number
}
37 changes: 37 additions & 0 deletions src/models/ShortUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FindOrCreate } from '@typegoose/typegoose/lib/defaultClasses'
import { getModelForClass, prop } from '@typegoose/typegoose'
import randomToken = require('random-token')
import checkAvailableResolutions from '@/helpers/checkAvailableResolutions'

export class ShortUrl extends FindOrCreate {
@prop({
required: true,
index: true,
unique: true,
default: () => randomToken(16),
})
shortId!: string
@prop({ required: true })
url!: string
@prop({ required: true, type: () => [Number] })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно обойтись без этого поля?

availableResolutions!: number[]
}

const ShortUrlModel = getModelForClass(ShortUrl, {
schemaOptions: { timestamps: true },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай использовать @modelOptions

})

export async function findOrCreateShortUrl(url: string) {
const shortUrl = await ShortUrlModel.findOne({ url })
if (shortUrl) {
return shortUrl
}

const availableResolutions = await checkAvailableResolutions(url)

return new ShortUrlModel({ url, availableResolutions }).save()
}

export async function findShortUrl(shortId: string) {
return ShortUrlModel.findOne({ shortId })
}
12 changes: 8 additions & 4 deletions src/models/Url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ export class Url {
audio!: boolean
@prop({ required: true })
title!: string
@prop({ index: true })
resolution?: number
}

const UrlModel = getModelForClass(Url, {
schemaOptions: { timestamps: true },
})

export function findUrl(url: string, audio: boolean) {
return UrlModel.findOne({ url, audio })
export function findUrl(url: string, audio: boolean, resolution?: number) {
return UrlModel.findOne({ url, audio, resolution })
}

export async function findOrCreateUrl(
url: string,
fileId: string,
audio: boolean,
title: string
title: string,
resolution?: number
) {
const dburl = await UrlModel.findOne({ url, audio })
const dburl = await UrlModel.findOne({ url, audio, resolution })
if (dburl) {
return dburl
}
Expand All @@ -34,5 +37,6 @@ export async function findOrCreateUrl(
fileId,
audio,
title,
resolution,
})
}
5 changes: 3 additions & 2 deletions src/models/downloadJobFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ export function findOrCreateDownloadJob(
url: string,
audio: boolean,
originalChatId: number,
originalMessageId: number
originalMessageId: number,
resolution?: number
) {
return DownloadJobModel.findOrCreate(
{ url, audio },
{ url, audio, resolution },
{ originalChatId, originalMessageId }
)
}
Expand Down
1 change: 1 addition & 0 deletions src/types/random-token.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'random-token'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нам это точно нужно? Мы же делаем require

Loading