support direct mp4 files
This commit is contained in:
parent
5de2148123
commit
5a5b0afaaa
121
downloader.py
121
downloader.py
@ -2,15 +2,16 @@
|
||||
# Downloads the video/audio as a stream from a provided link using yt-dlp
|
||||
# does not save the file, only the most recent fragment is held. Thus, this is
|
||||
# ideal for devices with little memory
|
||||
# TODO gather video metadata before download
|
||||
#
|
||||
|
||||
import ffmpeg
|
||||
import tempfile
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import pip
|
||||
import signal
|
||||
import json
|
||||
from logger import logger
|
||||
from threading import Thread, main_thread
|
||||
from time import sleep
|
||||
@ -26,39 +27,71 @@ dirpath = tempfile.mkdtemp()
|
||||
sys.path.append(dirpath)
|
||||
updateYtdlp()
|
||||
|
||||
class Downloader(Thread, StreamSource):
|
||||
baseOpts = [
|
||||
"-f", "bestaudio/best", # select for best audio
|
||||
# "--audio-format", "mp3", "-x", # cannot do because it requires a tmp file to re-encode
|
||||
"--prefer-ffmpeg",
|
||||
"--no-mark-watched",
|
||||
"--geo-bypass",
|
||||
"--no-playlist",
|
||||
"--retries", "100",
|
||||
"--extractor-retries", "100",
|
||||
# "--throttled-rate", "100K", # get around youtube throttling; probably not needed anymore
|
||||
"--no-call-home",
|
||||
"--sponsorblock-remove", "sponsor,intro,selfpromo,interaction,preview,music_offtopic",
|
||||
]
|
||||
|
||||
def runYtdlp(opts):
|
||||
# update yt-dlp
|
||||
# TODO: do this only every once in a while
|
||||
# updateYtdlp()
|
||||
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = dirpath
|
||||
cmd = [
|
||||
sys.executable,
|
||||
dirpath + "/bin/yt-dlp"
|
||||
] + baseOpts + opts
|
||||
popen = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
|
||||
logger.add(popen.stderr, "yt-dlp.log")
|
||||
return popen
|
||||
|
||||
def getVideoInfo(url):
|
||||
popen = runYtdlp([
|
||||
"-j", # dump all metadata as json
|
||||
url
|
||||
])
|
||||
|
||||
j = popen.communicate()[0]
|
||||
|
||||
# make sure the ytdlp instance is closed
|
||||
if popen.poll() is None: # is alive?
|
||||
os.killpg(os.getpgid(popen.pid), signal.SIGTERM) # kill
|
||||
popen.stdout.close()
|
||||
|
||||
# keys of interest (but not always present)
|
||||
# title, direct, extractor, upload_date
|
||||
# format_id (machine readable)
|
||||
# view_count, uploader, duration, view_count, average_rating
|
||||
# age_limit, like_count, dislike_count, thumbnail,
|
||||
|
||||
try:
|
||||
return json.loads(j)
|
||||
except:
|
||||
return None
|
||||
|
||||
# Downloads using yt-dlp
|
||||
class YtdlpDownloader(Thread, StreamSource):
|
||||
def __init__(self, url, cb):
|
||||
Thread.__init__(self)
|
||||
|
||||
# update yt-dlp
|
||||
# TODO: do this only every once in a while
|
||||
# updateYtdlp()
|
||||
|
||||
self.cb = cb
|
||||
self.exit = False
|
||||
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = dirpath
|
||||
cmd = [
|
||||
sys.executable,
|
||||
dirpath + "/bin/yt-dlp",
|
||||
self.popen = runYtdlp([
|
||||
"-o", "-", # output stream to stdout
|
||||
"-f", "bestaudio/best", # select for best audio
|
||||
# "--audio-format", "mp3", "-x", # cannot do because it requires a tmp file to re-encode
|
||||
"--prefer-ffmpeg",
|
||||
"--no-mark-watched",
|
||||
"--geo-bypass",
|
||||
"--no-playlist",
|
||||
"--retries", "100",
|
||||
"--extractor-retries", "100",
|
||||
# "--throttled-rate", "100K", # get around youtube throttling; probably not needed anymore
|
||||
"--no-call-home",
|
||||
"--sponsorblock-remove", "sponsor,intro,selfpromo,interaction,preview,music_offtopic",
|
||||
url
|
||||
]
|
||||
self.popen = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid)
|
||||
logger.add(self.popen.stderr, "yt-dlp.log")
|
||||
])
|
||||
self.start()
|
||||
|
||||
def isAlive(self):
|
||||
@ -79,4 +112,40 @@ class Downloader(Thread, StreamSource):
|
||||
sleep(0.1)
|
||||
|
||||
def getStream(self):
|
||||
return self.popen.stdout
|
||||
return self.popen.stdout
|
||||
|
||||
# Downloads the file and plays it (must be a direct URL, ex: cannot be a yt link)
|
||||
class DirectDownloader(Thread, StreamSource):
|
||||
def __init__(self, url, cb):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.cb = cb
|
||||
self.exit = False
|
||||
|
||||
self.process = ( ffmpeg
|
||||
.input(url)
|
||||
.output('pipe:', format='mp3')
|
||||
.run_async(pipe_stdout=True, pipe_stderr=True)
|
||||
)
|
||||
logger.add(self.process.stderr, "direct-downloader.log")
|
||||
|
||||
self.start()
|
||||
|
||||
def isAlive(self):
|
||||
return self.process.poll() is None
|
||||
|
||||
# checks to see if the current download has finished
|
||||
# if yes, cleans up and fires callback
|
||||
def run(self):
|
||||
while main_thread().is_alive() and not self.exit:
|
||||
if not self.isAlive():
|
||||
self.cb()
|
||||
sleep(0.1)
|
||||
|
||||
def stop(self):
|
||||
self.process.stdout.close()
|
||||
self.process.stderr.close()
|
||||
self.exit = True
|
||||
|
||||
def getStream(self):
|
||||
return self.process.stdout
|
8
radio.py
8
radio.py
@ -26,7 +26,13 @@ class Radio(object):
|
||||
# plays the next song in the queue
|
||||
def play(self):
|
||||
self.playingUrl = self.queue.get()
|
||||
self.downloader = downloader.Downloader(self.playingUrl, self.downloadFinished)
|
||||
info = downloader.getVideoInfo(self.playingUrl)
|
||||
if info is None:
|
||||
return self.play()
|
||||
elif "direct" in info and info["direct"] == True:
|
||||
self.downloader = downloader.DirectDownloader(self.playingUrl, self.downloadFinished)
|
||||
else:
|
||||
self.downloader = downloader.YtdlpDownloader(self.playingUrl, self.downloadFinished)
|
||||
self.transcoder = transcoder.Transcoder(self.downloader)
|
||||
self.buffer = buffer.Buffer(self.transcoder)
|
||||
self.uploader.setUpstream(self.buffer)
|
||||
|
@ -4,36 +4,29 @@ from stream_listener import StreamListener
|
||||
from stream import StreamSource
|
||||
import os
|
||||
|
||||
fifoFile = 'transcode-fifo'
|
||||
|
||||
# converts the stream to mp3 before sending to the long-lasting mp3 connection
|
||||
# write to a fifo file instead of directly via stdin to fool ffmpeg into thinking it has seekable input
|
||||
# https://lists.libav.org/pipermail/ffmpeg-user/2007-May/008917.html
|
||||
class Transcoder(StreamSource):
|
||||
|
||||
def __init__(self, upstream):
|
||||
if os.path.exists(fifoFile):
|
||||
os.remove(fifoFile)
|
||||
os.mkfifo(fifoFile)
|
||||
self.process = ( ffmpeg
|
||||
.input(fifoFile)
|
||||
.input('pipe:')
|
||||
.output('pipe:', format='mp3')
|
||||
.run_async(pipe_stdout=True, pipe_stderr=True)
|
||||
.run_async(pipe_stdin=True, pipe_stdout=True, pipe_stderr=True)
|
||||
)
|
||||
logger.add(self.process.stderr, "transcoder.log")
|
||||
self.listener = StreamListener(upstream, self)
|
||||
self.listener.start()
|
||||
self.file = open(fifoFile, 'wb')
|
||||
|
||||
def stop(self):
|
||||
self.file.close()
|
||||
self.listener.stop()
|
||||
self.process.stdin.close()
|
||||
self.process.stdout.close()
|
||||
self.process.stderr.close()
|
||||
os.remove(fifoFile)
|
||||
|
||||
def getStream(self):
|
||||
return self.process.stdout
|
||||
|
||||
def write(self, chunk):
|
||||
self.file.write(chunk)
|
||||
self.process.stdin.write(chunk)
|
Loading…
x
Reference in New Issue
Block a user