diff --git a/README.md b/README.md
index b25b2f88..3712d639 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-
Music Play Bot 🎵
+Telegram Group Music Player Bot 🎵
-### Here the advanced branch with more features 🙂
+### A bot that can play music on telegram group's voice call
-
+
Requirements 📝
@@ -16,7 +16,7 @@
### Commands 🛠
#### For all in group
- `/play` - reply to youtube url or song file to play song
-- `/ytp ` - play song without youtube url or song file (best method)
+- `/play ` - play song you requested
- `/song ` - download songs you want quickly
- `/search ` - search videos on youtube with details
@@ -28,14 +28,13 @@
### Deploy To Heroku
-[](https://heroku.com/deploy?template=https://github.com/InukaAsith/GroupMusicBot)
+[](https://heroku.com/deploy?template=https://github.com/Infinity-Bots/GroupMusicPlayerBot)
-Get pyrogram STRING_NAME from here ⬇️
-
-[](https://replit.com/@SpEcHiDe/GenerateStringSession)
+Use [Repl Link](https://replit.com/@SpEcHiDe/GenerateStringSession) to get pyrogram string session
### Credits
-- [ImJanindu](https://github.com/Imjanindu): Dev
+- [ImJanindu](https://github.com/ImJanindu): Dev
+- [InukaASiTH](https://github.com/InukaAsith): Dev
- [Laky](https://github.com/Laky-64) & [Andrew](https://github.com/AndrewLaneX): PyTgCalls
- [Original Repo](https://github.com/suprojects/CallsMusic)
- [Infinity BOTs](https://t.me/Infinity_BOTs)
diff --git a/etc/font.otf b/etc/font.otf
index 442742e0..3c356311 100644
Binary files a/etc/font.otf and b/etc/font.otf differ
diff --git a/etc/foreground.png b/etc/foreground.png
index 09aa8666..612d22dc 100644
Binary files a/etc/foreground.png and b/etc/foreground.png differ
diff --git a/etc/foreground_square.png b/etc/foreground_square.png
index 3ef6c94e..bfe66210 100644
Binary files a/etc/foreground_square.png and b/etc/foreground_square.png differ
diff --git a/etc/tg_vc_bot.png b/etc/tg_vc_bot.png
index e703a3f2..850fa088 100644
Binary files a/etc/tg_vc_bot.png and b/etc/tg_vc_bot.png differ
diff --git a/etc/thumb.jpg b/etc/thumb.jpg
new file mode 100644
index 00000000..46d1ca67
Binary files /dev/null and b/etc/thumb.jpg differ
diff --git a/handlers/inline.py b/handlers/inline.py
deleted file mode 100644
index efe9b275..00000000
--- a/handlers/inline.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from pyrogram import Client, errors
-from pyrogram.types import InlineQuery, InlineQueryResultArticle, InputTextMessageContent
-
-from youtubesearchpython import VideosSearch
-
-
-@Client.on_inline_query()
-async def inline(client: Client, query: InlineQuery):
- answers = []
- search_query = query.query.lower().strip().rstrip()
-
- if search_query == "":
- await client.answer_inline_query(
- query.id,
- results=answers,
- switch_pm_text="Type a YouTube video name...",
- switch_pm_parameter="help",
- cache_time=0
- )
- else:
- search = VideosSearch(search_query, limit=50)
-
- for result in search.result()["result"]:
- answers.append(
- InlineQueryResultArticle(
- title=result["title"],
- description="{}, {} views.".format(
- result["duration"],
- result["viewCount"]["short"]
- ),
- input_message_content=InputTextMessageContent(
- "https://www.youtube.com/watch?v={}".format(
- result["id"]
- )
- ),
- thumb_url=result["thumbnails"][0]["url"]
- )
- )
-
- try:
- await query.answer(
- results=answers,
- cache_time=0
- )
- except errors.QueryIdInvalid:
- await query.answer(
- results=answers,
- cache_time=0,
- switch_pm_text="Error: Search timed out",
- switch_pm_parameter="",
- )
diff --git a/handlers/play.py b/handlers/play.py
index 9f26efe9..01522027 100644
--- a/handlers/play.py
+++ b/handlers/play.py
@@ -1,34 +1,25 @@
+import os
from os import path
-
-from pyrogram import Client
-from pyrogram.types import Message, Voice
-
+from pyrogram import Client, filters
+from pyrogram.types import Message, Voice, InlineKeyboardButton, InlineKeyboardMarkup
+from pyrogram.errors import UserAlreadyParticipant
from callsmusic import callsmusic, queues
-
-from os import path
+from callsmusic.callsmusic import client as USER
+from helpers.admins import get_administrators
import requests
import aiohttp
import youtube_dl
from youtube_search import YoutubeSearch
-
-
import converter
from downloaders import youtube
-
-from config import BOT_NAME as bn, DURATION_LIMIT
-from helpers.filters import command, other_filters
+from config import DURATION_LIMIT
+from helpers.filters import command
from helpers.decorators import errors
from helpers.errors import DurationLimitError
from helpers.gets import get_url, get_file_name
-from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
-
-import os
-import aiohttp
import aiofiles
import ffmpeg
-from PIL import Image
-from PIL import ImageFont
-from PIL import ImageDraw
+from PIL import Image, ImageFont, ImageDraw
def transcode(filename):
@@ -83,7 +74,7 @@ async def generate_cover(requested_by, title, views, duration, thumbnail):
)
draw.text((190, 630), f"Views: {views}", (255, 255, 255), font=font)
draw.text((190, 670),
- f"Played By: {requested_by}",
+ f"Added By: {requested_by}",
(255, 255, 255),
font=font,
)
@@ -94,62 +85,92 @@ async def generate_cover(requested_by, title, views, duration, thumbnail):
-@Client.on_message(command("play") & other_filters)
-@errors
+@Client.on_message(command("play")
+ & filters.group
+ & ~filters.edited
+ & ~filters.forwarded
+ & ~filters.via_bot)
async def play(_, message: Message):
- lel = await message.reply("🔄 **Processing** sounds...")
- sender_id = message.from_user.id
- sender_name = message.from_user.first_name
-
- keyboard = InlineKeyboardMarkup(
- [
- [
- InlineKeyboardButton(
- text="🔊 Channel",
- url="https://t.me/Infinity_BOTs")
-
- ]
- ]
- )
-
+ lel = await message.reply("🔄 **Processing...**")
+
+ administrators = await get_administrators(message.chat)
+ chid = message.chat.id
+
+ try:
+ user = await USER.get_me()
+ except:
+ user.first_name = "Mizuki"
+ usar = user
+ wew = usar.id
+ try:
+ await _.get_chat_member(chid, wew)
+ except:
+ for administrator in administrators:
+ if administrator == message.from_user.id:
+ try:
+ invitelink = await _.export_chat_invite_link(chid)
+ except:
+ await lel.edit(
+ "Add me as admin of yor group first!")
+ return
+
+ try:
+ await USER.join_chat(invitelink)
+ await USER.send_message(
+ message.chat.id, "**Mizuki Music assistant joined this group for play music 🎵**")
+
+ except UserAlreadyParticipant:
+ pass
+ except Exception:
+ await lel.edit(
+ f"🛑 Flood Wait Error 🛑 \n\Hey {user.first_name}, assistant userbot couldn't join your group due to heavy join requests. Make sure userbot is not banned in group and try again later!")
+ try:
+ await USER.get_chat(chid)
+ except:
+ await lel.edit(
+ f"Hey {user.first_name}, assistant userbot is not in this chat, ask admin to send /play command for first time to add it.")
+ return
+
audio = (message.reply_to_message.audio or message.reply_to_message.voice) if message.reply_to_message else None
url = get_url(message)
if audio:
if round(audio.duration / 60) > DURATION_LIMIT:
raise DurationLimitError(
- f"❌ Videos longer than {DURATION_LIMIT} minute(s) aren't allowed to play!"
+ f"❌ Videos longer than {DURATION_LIMIT} minutes aren't allowed to play!"
)
file_name = get_file_name(audio)
title = file_name
- thumb_name = "https://telegra.ph/file/a4fa687ed647cfef52402.jpg"
+ thumb_name = "https://telegra.ph/file/caeb50039026a746e7252.jpg"
thumbnail = thumb_name
duration = round(audio.duration / 60)
views = "Locally added"
+
keyboard = InlineKeyboardMarkup(
+ [
[
- [
- InlineKeyboardButton(
- text="🔊 Channel",
- url=f"https://t.me/Daisyxupdates")
-
- ]
+ InlineKeyboardButton(
+ text="Channel 🔊",
+ url="https://t.me/Infinity_BOTs")
+
]
- )
- requested_by = message.from_user.mention()
+ ]
+ )
+
+ requested_by = message.from_user.first_name
await generate_cover(requested_by, title, views, duration, thumbnail)
file_path = await converter.convert(
(await message.reply_to_message.download(file_name))
if not path.isfile(path.join("downloads", file_name)) else file_name
)
+
elif url:
try:
results = YoutubeSearch(url, max_results=1).to_dict()
- # url = f"https://youtube.com{results[0]['url_suffix']}"
- #print(results)
- title = results[0]["title"][:40]
+ # print results
+ title = results[0]["title"]
thumbnail = results[0]["thumbnails"][0]
thumb_name = f'thumb{title}.jpg'
thumb = requests.get(thumbnail, allow_redirects=True)
@@ -157,53 +178,60 @@ async def play(_, message: Message):
duration = results[0]["duration"]
url_suffix = results[0]["url_suffix"]
views = results[0]["views"]
+ durl = url
+ durl = durl.replace("youtube", "youtubepp")
+
+ secmul, dur, dur_arr = 1, 0, duration.split(':')
+ for i in range(len(dur_arr)-1, -1, -1):
+ dur += (int(dur_arr[i]) * secmul)
+ secmul *= 60
+
keyboard = InlineKeyboardMarkup(
+ [
[
- [
- InlineKeyboardButton(
- text="Watch On YouTube 🎬",
- url=f"{url}")
+ InlineKeyboardButton(
+ text="YouTube 🎬",
+ url=f"{url}"),
+ InlineKeyboardButton(
+ text="Download 📥",
+ url=f"{durl}")
- ]
]
- )
+ ]
+ )
except Exception as e:
title = "NaN"
- thumb_name = "https://telegra.ph/file/a4fa687ed647cfef52402.jpg"
+ thumb_name = "https://telegra.ph/file/638c20c44ca418c8b2178.jpg"
duration = "NaN"
views = "NaN"
keyboard = InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
- text="Watch On YouTube 🎬",
+ text="YouTube 🎬",
url=f"https://youtube.com")
]
]
)
- requested_by = message.from_user.mention()
+ if (dur / 60) > DURATION_LIMIT:
+ await lel.edit(f"❌ Videos longer than {DURATION_LIMIT} minutes aren't allowed to play!")
+ return
+ requested_by = message.from_user.first_name
await generate_cover(requested_by, title, views, duration, thumbnail)
file_path = await converter.convert(youtube.download(url))
else:
- await lel.edit("🔎 **Finding** the song...")
- sender_id = message.from_user.id
- user_id = message.from_user.id
- sender_name = message.from_user.first_name
- user_name = message.from_user.first_name
- rpk = "["+user_name+"](tg://user?id="+str(user_id)+")"
-
- query = ''
- for i in message.command[1:]:
- query += ' ' + str(i)
- print(query)
- await lel.edit("🎵 **Processing** sounds...")
- ydl_opts = {"format": "bestaudio[ext=m4a]"}
+ if len(message.command) < 2:
+ return await lel.edit("🧐 **What's the song you want to play?**")
+ await lel.edit("🔎 **Finding the song...**")
+ query = message.text.split(None, 1)[1]
+ # print(query)
+ await lel.edit("🎵 **Processing sounds...**")
try:
results = YoutubeSearch(query, max_results=1).to_dict()
url = f"https://youtube.com{results[0]['url_suffix']}"
- #print(results)
- title = results[0]["title"][:40]
+ # print results
+ title = results[0]["title"]
thumbnail = results[0]["thumbnails"][0]
thumb_name = f'thumb{title}.jpg'
thumb = requests.get(thumbnail, allow_redirects=True)
@@ -211,9 +239,16 @@ async def play(_, message: Message):
duration = results[0]["duration"]
url_suffix = results[0]["url_suffix"]
views = results[0]["views"]
-
+ durl = url
+ durl = durl.replace("youtube", "youtubepp")
+
+ secmul, dur, dur_arr = 1, 0, duration.split(':')
+ for i in range(len(dur_arr)-1, -1, -1):
+ dur += (int(dur_arr[i]) * secmul)
+ secmul *= 60
+
except Exception as e:
- lel.edit(
+ await lel.edit(
"❌ Song not found.\n\nTry another song or maybe spell it properly."
)
print(str(e))
@@ -223,13 +258,20 @@ async def play(_, message: Message):
[
[
InlineKeyboardButton(
- text="Watch On YouTube 🎬",
- url=f"{url}")
+ text="YouTube 🎬",
+ url=f"{url}"),
+ InlineKeyboardButton(
+ text="Download 📥",
+ url=f"{durl}")
]
]
)
- requested_by = message.from_user.mention()
+
+ if (dur / 60) > DURATION_LIMIT:
+ await lel.edit(f"❌ Videos longer than {DURATION_LIMIT} minutes aren't allowed to play!")
+ return
+ requested_by = message.from_user.first_name
await generate_cover(requested_by, title, views, duration, thumbnail)
file_path = await converter.convert(youtube.download(url))
@@ -237,7 +279,9 @@ async def play(_, message: Message):
position = await queues.put(message.chat.id, file=file_path)
await message.reply_photo(
photo="final.png",
- caption=f"#⃣ Your requested song **queued** at position {position}!",
+ caption="**🎵 Song:** {}\n**🕒 Duration:** {} min\n**👤 Added By:** {}\n\n**#⃣ Queued Position:** {}".format(
+ title, duration, message.from_user.mention(), position
+ ),
reply_markup=keyboard)
os.remove("final.png")
return await lel.delete()
@@ -246,9 +290,8 @@ async def play(_, message: Message):
await message.reply_photo(
photo="final.png",
reply_markup=keyboard,
- caption="▶️ **Playing** here the song requested by {} via DaisyX Music 😜".format(
- message.from_user.mention()
- ),
- )
+ caption="**🎵 Song:** {}\n**🕒 Duration:** {} min\n**👤 Added By:** {}\n\n**▶️ Now Playing at `{}`...**".format(
+ title, duration, message.from_user.mention(), message.chat.title
+ ), )
os.remove("final.png")
return await lel.delete()
diff --git a/handlers/private.py b/handlers/private.py
index 3b82c87f..18d5089d 100644
--- a/handlers/private.py
+++ b/handlers/private.py
@@ -19,7 +19,7 @@ async def start(_, message: Message):
[
[
InlineKeyboardButton(
- "🛠 Source Code 🛠", url="https://github.com/ImJanindu/GroupMusicBot")
+ "🛠 Source Code 🛠", url="https://github.com/Infinity-Bots/GroupMusicPlayerBot")
],[
InlineKeyboardButton(
"💬 Group", url="https://t.me/InfinityBOTs_Support"
diff --git a/handlers/songs.py b/handlers/songs.py
index 8c8d1885..1ec1781c 100644
--- a/handlers/songs.py
+++ b/handlers/songs.py
@@ -1,69 +1,104 @@
+# Infinity Bots (https://t.me/Infinity_Bots)
+
import os
-import requests
import aiohttp
-import youtube_dl
-
+import asyncio
+import json
+import sys
+import time
+from youtubesearchpython import SearchVideos
from pyrogram import filters, Client
-from youtube_search import YoutubeSearch
-
-def time_to_seconds(time):
- stringt = str(time)
- return sum(int(x) * 60 ** i for i, x in enumerate(reversed(stringt.split(':'))))
-
-
-@Client.on_message(filters.command('song') & ~filters.private & ~filters.channel)
-def song(client, message):
-
- user_id = message.from_user.id
- user_name = message.from_user.first_name
- rpk = "["+user_name+"](tg://user?id="+str(user_id)+")"
+from youtube_dl import YoutubeDL
+from youtube_dl.utils import (
+ ContentTooShortError,
+ DownloadError,
+ ExtractorError,
+ GeoRestrictedError,
+ MaxDownloadsReached,
+ PostProcessingError,
+ UnavailableVideoError,
+ XAttrMetadataError,
+)
- query = ''
- for i in message.command[1:]:
- query += ' ' + str(i)
- print(query)
- m = message.reply('🔎 Finding the song...')
- ydl_opts = {"format": "bestaudio[ext=m4a]"}
+@Client.on_message(filters.command("song") & ~filters.edited)
+async def song(client, message):
+ cap = "@JEBotZ"
+ url = message.text.split(None, 1)[1]
+ rkp = await message.reply("Processing...")
+ if not url:
+ await rkp.edit("**What's the song you want?**\nUsage`/song `")
+ search = SearchVideos(url, offset=1, mode="json", max_results=1)
+ test = search.result()
+ p = json.loads(test)
+ q = p.get("search_result")
try:
- results = YoutubeSearch(query, max_results=1).to_dict()
- link = f"https://youtube.com{results[0]['url_suffix']}"
- #print(results)
- title = results[0]["title"][:40]
- thumbnail = results[0]["thumbnails"][0]
- thumb_name = f'thumb{title}.jpg'
- thumb = requests.get(thumbnail, allow_redirects=True)
- open(thumb_name, 'wb').write(thumb.content)
-
-
- duration = results[0]["duration"]
- url_suffix = results[0]["url_suffix"]
- views = results[0]["views"]
-
- except Exception as e:
- m.edit(
- "❌ Found Nothing.\n\nTry another keywork or maybe spell it properly."
+ url = q[0]["link"]
+ except BaseException:
+ return await rkp.edit("Failed to find that song.")
+ type = "audio"
+ if type == "audio":
+ opts = {
+ "format": "bestaudio",
+ "addmetadata": True,
+ "key": "FFmpegMetadata",
+ "writethumbnail": True,
+ "prefer_ffmpeg": True,
+ "geo_bypass": True,
+ "nocheckcertificate": True,
+ "postprocessors": [
+ {
+ "key": "FFmpegExtractAudio",
+ "preferredcodec": "mp3",
+ "preferredquality": "320",
+ }
+ ],
+ "outtmpl": "%(id)s.mp3",
+ "quiet": True,
+ "logtostderr": False,
+ }
+ song = True
+ try:
+ await rkp.edit("Downloading...")
+ with YoutubeDL(opts) as rip:
+ rip_data = rip.extract_info(url)
+ except DownloadError as DE:
+ await rkp.edit(f"`{str(DE)}`")
+ return
+ except ContentTooShortError:
+ await rkp.edit("`The download content was too short.`")
+ return
+ except GeoRestrictedError:
+ await rkp.edit(
+ "`Video is not available from your geographic location due to geographic restrictions imposed by a website.`"
)
- print(str(e))
return
- m.edit("Downloading the song by @Infinity_BOTs...")
- try:
- with youtube_dl.YoutubeDL(ydl_opts) as ydl:
- info_dict = ydl.extract_info(link, download=False)
- audio_file = ydl.prepare_filename(info_dict)
- ydl.process_info(info_dict)
- rep = '**🎵 Uploaded by @Infinity_BOTs**'
- secmul, dur, dur_arr = 1, 0, duration.split(':')
- for i in range(len(dur_arr)-1, -1, -1):
- dur += (int(dur_arr[i]) * secmul)
- secmul *= 60
- message.reply_audio(audio_file, caption=rep, thumb=thumb_name, parse_mode='md', title=title, duration=dur)
- m.delete()
- except Exception as e:
- m.edit('❌ Error')
- print(e)
-
- try:
- os.remove(audio_file)
- os.remove(thumb_name)
+ except MaxDownloadsReached:
+ await rkp.edit("`Max-downloads limit has been reached.`")
+ return
+ except PostProcessingError:
+ await rkp.edit("`There was an error during post processing.`")
+ return
+ except UnavailableVideoError:
+ await rkp.edit("`Media is not available in the requested format.`")
+ return
+ except XAttrMetadataError as XAME:
+ await rkp.edit(f"`{XAME.code}: {XAME.msg}\n{XAME.reason}`")
+ return
+ except ExtractorError:
+ await rkp.edit("`There was an error during info extraction.`")
+ return
except Exception as e:
- print(e)
+ await rkp.edit(f"{str(type(e)): {str(e)}}")
+ return
+ time.time()
+ if song:
+ await rkp.edit("Uploading...") #ImJanindu
+ lol = "./etc/thumb.jpg"
+ lel = await message.reply_audio(
+ f"{rip_data['id']}.mp3",
+ duration=int(rip_data["duration"]),
+ title=str(rip_data["title"]),
+ performer=str(rip_data["uploader"]),
+ thumb=lol,
+ caption=cap) #JEBotZ
+ await rkp.delete()
diff --git a/handlers/ytplay b/handlers/ytplay
deleted file mode 100644
index 08201a3e..00000000
--- a/handlers/ytplay
+++ /dev/null
@@ -1,108 +0,0 @@
-import os
-from os import path
-import requests
-import aiohttp
-import youtube_dl
-from pyrogram import Client
-from pyrogram.types import Message, Voice
-from youtube_search import YoutubeSearch
-from callsmusic import callsmusic, queues
-
-import converter
-from downloaders import youtube
-
-from config import BOT_NAME as bn, DURATION_LIMIT
-from helpers.filters import command, other_filters
-from helpers.decorators import errors
-from helpers.errors import DurationLimitError
-from helpers.gets import get_url, get_file_name
-from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
-
-@Client.on_message(command("ytp") & other_filters)
-@errors
-async def play(_, message: Message):
-
- lel = await message.reply("🔎 **Finding** the song...")
- sender_id = message.from_user.id
- user_id = message.from_user.id
- sender_name = message.from_user.first_name
- user_name = message.from_user.first_name
- rpk = "["+user_name+"](tg://user?id="+str(user_id)+")"
-
- query = ''
- for i in message.command[1:]:
- query += ' ' + str(i)
- print(query)
- await lel.edit("🎵 **Processing** sounds...")
- ydl_opts = {"format": "bestaudio[ext=m4a]"}
- try:
- results = YoutubeSearch(query, max_results=1).to_dict()
- url = f"https://youtube.com{results[0]['url_suffix']}"
- #print(results)
- title = results[0]["title"][:40]
- thumbnail = results[0]["thumbnails"][0]
- thumb_name = f'thumb{title}.jpg'
- thumb = requests.get(thumbnail, allow_redirects=True)
- open(thumb_name, 'wb').write(thumb.content)
-
-
- duration = results[0]["duration"]
- url_suffix = results[0]["url_suffix"]
- views = results[0]["views"]
-
- except Exception as e:
- lel.edit(
- "❌ Song not found.\n\nTry another song or maybe spell it properly."
- )
- print(str(e))
- return
-
- keyboard = InlineKeyboardMarkup(
- [
- [
- InlineKeyboardButton(
- text="Watch On YouTube 🎬",
- url=f"{url}")
-
- ]
- ]
- )
-
- keyboard2 = InlineKeyboardMarkup(
- [
- [
- InlineKeyboardButton(
- text="Watch On YouTube 🎬",
- url=f"{url}")
-
- ]
- ]
- )
-
- audio = (message.reply_to_message.audio or message.reply_to_message.voice) if message.reply_to_message else None
-
- if audio:
- await lel.edit_text("Lel")
-
- elif url:
- file_path = await converter.convert(youtube.download(url))
- else:
- return await lel.edit_text("❗ You did not give me anything to play!")
-
- if message.chat.id in callsmusic.pytgcalls.active_calls:
- position = await queues.put(message.chat.id, file=file_path)
- await message.reply_photo(
- photo=thumb_name,
- caption=f"#⃣ Your requested song **queued** at position {position}!",
- reply_markup=keyboard2)
- return await lel.delete()
- else:
- callsmusic.pytgcalls.join_group_call(message.chat.id, file_path)
- await message.reply_photo(
- photo=thumb_name,
- reply_markup=keyboard,
- caption="▶️ **Playing** here the song requested by {} via YouTube Music 😜".format(
- message.from_user.mention()
- ),
- )
- return await lel.delete()
diff --git a/requirements.txt b/requirements.txt
index 1a6cec23..7f2a3a13 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
pyrogram
TgCrypto
-py-tgcalls
+py-tgcalls==0.5.2
python-dotenv
youtube_dl
youtube_search_python
@@ -12,3 +12,4 @@ youtube_search
search_engine_parser
ffmpeg
Pillow
+ujson