How to stream audio from a Youtube URL in Python (without download)?
Asked Answered
T

4

14

I have been trying to create a way to stream a youtube url (preferably audio only, although this doesn't matter too much) right from python code. I have tried numerous things but none really seem to work. So far, I am able to search for videos or playlists using youtube data api, grab the first video or playlist and pass it into pafy to get different streaming urls. Does anyone know of a way to play youtube audio/video through python without downloading the video first? I would think it is possible with a cmd line tool such as mplayer or vlc using the sub process to pop open a cmd for the cmd line and pass in the url, but I am stuck. Any help is needed. Please! Here is my following code:

import argparse
import pafy
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

DEVELOPER_KEY = 'DEVELOPER KEY'
YOUTUBE_API_SERVICE_NAME = 'youtube'
YOUTUBE_API_VERSION = 'v3'

def pafy_video(video_id):
    url = 'https://www.youtube.com/watch?v={0}'.format(video_id)
    vid = pafy.new(url)

def pafy_playlist(playlist_id)
    url = "https://www.youtube.com/playlist?list={0}".format(playlist_id)
    playlist = pafy.get_playlist(url)


def youtube_search(options):
  youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=DEVELOPER_KEY)

  search_response = youtube.search().list(
    q='Hello world',
    part='id,snippet',
    maxResults=options.max_results
  ).execute()

  videos = []
  playlists = []
  channels = []
  for search_result in search_response.get('items', []):
    if search_result['id']['kind'] == 'youtube#video':
      videos.append('%s' % (search_result['id']['videoId']))
    elif search_result['id']['kind'] == 'youtube#channel':
      channels.append('%s' % (search_result['id']['channelId']))
    elif search_result['id']['kind'] == 'youtube#playlist':
      playlists.append('%s' % (search_result['id']['playlistId']))


  if videos:
    print('Videos:{0}'.format(videos))
    pafy_video(videos[0])
  elif playlists:
    print('Playlists:{0}'.format(playlists))
    pafy_video(playlists[0])

  #https://www.youtube.com/watch?v=rOU4YiuaxAM
  #url = 'https://www.youtube.com/watch?v={0}'.format(videos[0])
  #print(url)

if __name__ == '__main__':
  parser = argparse.ArgumentParser()
  parser.add_argument('--q', help='Search term', default='Google')
  parser.add_argument('--max-results', help='Max results', default=3)
  args = parser.parse_args()
  youtube_search(args)

Tldr; I would like to stream a youtube video (using the url or id) straight from python code without downloading the video first

Thank you!

Token answered 19/3, 2018 at 0:28 Comment(0)
P
15

pafy according to its documentation do not list playing media directly (at least I didn't find any).

However we can use it to get correct url, and then use player such as vlc to play directly without downloading.

You can download vlc from here

First we get correct / best URL from youtube using pafy

import pafy
import vlc

url = "https://www.youtube.com/watch?v=bMt47wvK6u0"
video = pafy.new(url)
best = video.getbest()
playurl = best.url

Over here playurl is best URL to play. Then we use VLC to play it.

Instance = vlc.Instance()
player = Instance.media_player_new()
Media = Instance.media_new(playurl)
Media.get_mrl()
player.set_media(Media)
player.play()

This will open a window with no controls (play/pause/stop etc). You can run these command on the repr window or at python prompt (depending upon how you are using it)
You will need to build one accordingly using vlc commands such as

>>> player.pause() #-- to pause video
>>> player.play()  #-- resume paused video. On older versions, 
                   #     this function was called resume
>>> player.stop()  #-- to stop/end video
Passmore answered 19/3, 2018 at 0:59 Comment(8)
Hey anil. Thank you! I have been reading about vlc. Do you know if it works on windows? When I installed it and ran it in my python script I got a windows os error. OSError: [WinError 126] The specified module could not be found. If it is not able to be played on windows, is there an alternative to vlc?Token
Hey Nick. I've window 7 and been using vlc for a while. So yes, it works. Matter of getting it installed correctly. Try installing using pip install python-vlc.Passmore
Alright thanks anil! I downloaded regular vlc for windows and I have ran pip install python-vlc but still no luck. I continue to get that error. I then tried including the vlc.py right in my directory but then got the error that vlc has no object "instance". Do you think you could point me to any guides on installing python vlc? I'm sorry for the hassle but thank you for everything!Token
As per this thread https://mcmap.net/q/829099/-python-bindings-for-vlc , looks like you may be trying to install 32bit VLC on 64bit python. You may be having vlc.exe (vlc player app on windows). you may want to remove all kind of vlc (app, python packages etc). and then start fresh. Always look for previous answers on SO. See one here: superuser.com/questions/1281731/…Passmore
I am getting the same exact error under Windows. I have 32bit Python 2.7 installed. This solution doesn't workLinwoodlinz
@Passmore I am little late here, but I tried this solution using Python 3.8 and VLC 3.0.9, and this doesn't seem to work. I get an error saying - http stream error: local stream 1 error: Cancellation (0x8) when I try to stream from a youtube URL. Is youtube trying to stop bots or is it something else? But if I use vlc -v <url> on terminal, it works PS: I am using Pop!_OS (an ubuntu based distro)Halfprice
I found the problem with that. If you are using 64 bit, then install vlc, then install a 64 bit vlc player here: get.videolan.org/vlc/3.0.11/win64/vlc-3.0.11-win64.exe Do that, and there should be no error.Seel
since youtube is now hiding dislike counts, pafy.new(url) is not working. how can I fix that?Epizoic
K
8

The best way to accomplish this is to use mpv with youtube-dl (a light weight media player that comes with a command line console which allows you to stream any type of video/audio codecs via copy and pasting url to mpv console (works for cmd and terminal). Usage: path\to\mpv https://your/video/audioURL.com/

First import these modules - install via pip install bs4 requests

import re, requests, subprocess, urllib.parse, urllib.request
from bs4 import BeautifulSoup

Then create a variable to store music title of your choice

music_name = "Linkin Park Numb"
query_string = urllib.parse.urlencode({"search_query": music_name})

The goal here to use this search query to extract the output title and link of the first video result. We can then endcode the youtube url "https://www.youtube.com/results?search_query=" with the identifier using urllib (eg., "https://www.youtube.com/results?search_query=linkin+park+numb")

formatUrl = urllib.request.urlopen("https://www.youtube.com/results?" + query_string)

This right here re.findall(r"watch\?v=(\S{11})" views the 11 character identifier of all the video results from our query

search_results = re.findall(r"watch\?v=(\S{11})", formatUrl.read().decode())

After decoding the content, we can extract the full url by concatenating the main youtube url with the 11 character identifier

clip = requests.get("https://www.youtube.com/watch?v=" + "{}".format(search_results[0]))
clip2 = "https://www.youtube.com/watch?v=" + "{}".format(search_results[0])

print(clip2)
  • Output ==> https://www.youtube.com/watch?v=kXYiU_JCYtU

To further inspect the content, we can use beautifulsoup to scrape the extact title of the video

inspect = BeautifulSoup(clip.content, "html.parser")
yt_title = inspect.find_all("meta", property="og:title")

for concatMusic1 in yt_title:
    pass

print(concatMusic1['content'])
  • Output ==> Numb (Official Video) - Linkin Park

Finally, to play our music we input this to the command line

subprocess.Popen(
"start /b " + "path\\to\\mpv.exe" + clip2 + " --no-video --loop=inf --input-ipc-server=\\\\.\\pipe\\mpv-pipe > output.txt",
shell=True)

Full script

===================================================================

import re, requests, subprocess, urllib.parse, urllib.request
from bs4 import BeautifulSoup

music_name = "Linkin Park Numb"
query_string = urllib.parse.urlencode({"search_query": music_name})
formatUrl = urllib.request.urlopen("https://www.youtube.com/results?" + query_string)

search_results = re.findall(r"watch\?v=(\S{11})", formatUrl.read().decode())
clip = requests.get("https://www.youtube.com/watch?v=" + "{}".format(search_results[0]))
clip2 = "https://www.youtube.com/watch?v=" + "{}".format(search_results[0])

inspect = BeautifulSoup(clip.content, "html.parser")
yt_title = inspect.find_all("meta", property="og:title")

for concatMusic1 in yt_title:
    pass

print(concatMusic1['content'])

subprocess.Popen(
"start /b " + "path\\to\\mpv.exe " + clip2 + " --no-video --loop=inf --input-ipc-server=\\\\.\\pipe\\mpv-pipe > output.txt",
shell=True)


# Alternatively, you can do this for simplicity sake:
# subprocess.Popen("start /b " + "path\\to\\mpv.exe " + clip2 + "--no-video", shell=True)
K2 answered 24/8, 2020 at 20:36 Comment(0)
B
1

I made exactly this, including options like moving through the video with a scale, play/pause function, making playlist, shuffle function. One thing to know is that youtube_dl has trouble with some videos, there is a fork called yt_dlp which works better. For my app, I changed pafy so that it uses yt_dlp instead of youtube_dl. I can share the whole code, it's a lot, but it's pretty good I think.

#! /usr/bin/python3
from tkinter import *
from bs4 import BeautifulSoup
from youtube_search import YoutubeSearch
import re,random, vlc, pafy, datetime, time,yt_dlp


#global vars  
ran = 0
x = []
win = Tk()
count = 0
count2=-1
playurl = ""
scalevar = ""
scalevar2= 100
url2 = []
dur = 0

#window, player
win.geometry('610x100') 
win.title("Youtube Player")
menubar = Menu(win)
instance = vlc.Instance()
player = instance.media_player_new() 

#toggling shuffle
def funcRAN():
    global ran
    ran = not(ran)





#this function keeps track of time and moves the timescale
def every_second():
    global count2
    global ran
    length = player.get_length()
    place = player.get_time()
    if player.is_playing() == 0 and len(url2) > (count2 + 1) and len(url2) > 0  and abs(place-length) < 10000:
        if ran ==0:
            count2 += 1
        else:
            counttemp = count2
            while counttemp == count2:
                count2 = random.randrange(len(url2))

        scale.set(1)
        player.set_time(1)
        xvar = funcGO3(url2[count2], playurl)
        player.set_time(xvar)
        place = 1
    if player.is_playing() == 1:
        timee =str(datetime.timedelta(seconds = round(place/1000))) + " / " + str(datetime.timedelta(seconds = round(length/1000)))
        my_label2.config(text=timee)
        scale.set(place)
    win.after(1000,lambda:every_second())

def settime(scalevar):
    place = player.get_time()
    scalevar = int(scalevar)
    if abs(scalevar - place) > 4000:
        player.set_time(scalevar)

def songskip(amount):
    global count2
    global ran
    win.focus()
    if ran == 0:
        skip = count2 + amount
    else:
        counttemp = count2
        while counttemp == count2:
            count2 = random.randrange(len(url2))
            skip = count2

    if amount == -1 and player.get_time() > 10000:
        player.set_time(0)
    elif skip >= 0 and skip < len(url2):
        count2 = skip
        funcGO3(url2[skip], playurl)



#this function is for downloading the song
def funcDL(song_url, song_title):
    outtmpl = song_title + '.%(ext)s'
    ydl_opts = {
        'format': 'bestaudio/best',
        'outtmpl': outtmpl,
        'postprocessors': [
            {'key': 'FFmpegExtractAudio','preferredcodec': 'mp3',
             'preferredquality': '192',
            },
            {'key': 'FFmpegMetadata'},
        ],
    }

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info_dict = ydl.extract_info("youtube.com" + song_url, download=True) 


#this function is for moving back and forth in time of the song
def funcSKIP(amount):
    win.focus()
    timee = player.get_time() + amount
    if timee < 0:
        timee = 0
    timeall = player.get_length()
    if timee>timeall:
        timee = timeall 
    scale.set(timee)

#to pause
def funcPPTRANSFER():
    if str(win.focus_get()) != str(".!entry"):
        funcPP()
    
#to pause
def funcPP():
    win.focus()
    pause = player.is_playing()
    player.set_pause(pause)
    if pause == 1:
        btnPP.config(text="|>")
    else:
        btnPP.config(text="||")

#moving through the songlist
def funcMOVE(title, move, resultcount):
    win.focus()
    global count
    count += move
    if count < 0:
        count =resultcount
    if count > resultcount:
        count = 0
    my_label.config(text = title[count])


#starting the song
def funcGO(title, URL,  playurl):
    global count2
    global count
    win.focus()
    instance.vlm_stop_media(playurl)    
    URL = URL[count]
    url = "https://www.youtube.com" + str(URL) 
    count2 += 1
    url2.insert(count2,url)      
    video = pafy.new(url)                                                                                                                   
    #best = video.getbestaudio()
    best = video.audiostreams[0]
    print(best.quality, int(best.get_filesize()/10000)/100, "mb")
    printer = video.title 
    playurl = best.url                                                                                                                                                                                                                            
    media=instance.media_new(playurl)
    media.get_mrl()
    player.set_media(media)
    btnPP.place(x=340, y=2)
    btnGO2.place(x=170, y=62)
    btnBACK.place(x=295, y=2)
    btnBACK2.place(x=240, y=2)
    btnFWD.place(x=395, y=2)
    btnFWD2.place(x=445, y=2)
    scale.place(x=370, y=68)
    player.play()
    btnPP.config(text="||")
    scale.set(0)
    while player.is_playing() == 0:
        time.sleep(1)
    scale.config(to = player.get_length())
    win.title(title[count])
    

#this is to select the next song in the list
def funcGO2(URL):
    global url2
    global count
    win.focus()  
    URL2 = URL[count]
    url2.append("https://www.youtube.com" + str(URL2))
                                                                                   

#this is to play the next song in the list
def funcGO3(url, playurl):
    win.focus()
    instance.vlm_stop_media(playurl)                                                                                     
    video = pafy.new(url)                                                                                                                   
    #best = video.getbestaudio()
    best = video.audiostreams[0]
    print(best.quality, int(best.get_filesize()/10000)/100, "mb")
    printer = video.title 
    playurl = best.url                                                                                                                                                                                                                            
    media=instance.media_new(playurl)
    media.get_mrl()
    btnPP.place(x=340, y=2)
    btnGO2.place(x=170, y=62)
    btnBACK.place(x=295, y=2)
    btnBACK2.place(x=240, y=2)
    btnFWD.place(x=395, y=2)
    btnFWD2.place(x=445, y=2)
    scale.place(x=370, y=68)
    player.set_media(media)
    player.set_time(1)
    player.play()
    player.set_time(1)
    btnPP.config(text="||")
    scale.set(0)
    scalevar = scale.get()
    while player.is_playing() == 0:
        time.sleep(1)
    player.set_time(1)
    length = player.get_length()
    scale.config(to = length)
    timee =str(datetime.timedelta(seconds = 0)) + " / " + str(datetime.timedelta(seconds = round(length/1000)))
    my_label2.config(text=timee)
    win.title(printer)
    #win.after(2000, lambda:every_second(player, scale, count2))
    return(scalevar)

#controlling the volume
def funcSCALE(scalevar2):
    player.audio_set_volume(int(scalevar2))

#extracting info from the html of the search
def titlee(URL, question):
    results = YoutubeSearch(URL, max_results=40).to_dict()
    title=[]
    URL=[]
    resultcount = -1
    for v in results:
        length = v['duration']
        if length.count(':') > 1 and dur == 1:
            continue
        if length.count(':') == 1:
            m, s = length.split(':')
            length =  int(m) * 60 + int(s)
            if length > 300 and dur == 1:
                continue
        URL.append(v['url_suffix'])
        resultcount+= 1
        print(resultcount)
        title.append(re.sub(r"[^a-zA-Z0-9 .,:;+-=!?/()öäßü]", "", v['title']))
        if question == 1:
            url2.append("https://www.youtube.com" + str(v['url_suffix']))
        if resultcount == 20:
            break
    btnDL.place(x=505, y=2) 
    return(URL, title, 0, resultcount)

#making the search
def func(event):
    global count
    global x
    btnGO.focus()
    btnDL.place(x=500, y=2)
    x = str(var.get())
    URL, title, count, resultcount = titlee(x, 0)
    btnL.config(command = (lambda: funcMOVE(title, -1, resultcount)))
    btnR.config(command = (lambda:funcMOVE(title, 1, resultcount)))
    btnGO.config(command = (lambda: funcGO(title,URL,  playurl)))
    btnGO2.config(command = (lambda: funcGO2(URL)))
    btnDL.config(command =(lambda: funcDL(URL[count],title[count])))
    my_label.config(text = title[count])

#import all songs from querie
def funcImportall():
    global x
    titlee(x, 1)

#toggle limit duration of song
def funcDur():
    global dur
    dur = not(dur)

#clear playlist
def funcClear():
    global url2
    url2 = []


btnPP = Button(win, text = "||", command =(lambda: funcPP()))
btnBACK = Button(win, text = "<", command =(lambda: funcSKIP(-10000)))
btnBACK2 = Button(win, text = "<<", command =(lambda: songskip(-1)))
btnFWD = Button(win, text = ">", command =(lambda: funcSKIP(10000)))
btnFWD2 = Button(win, text = ">>", command =(lambda:  songskip(1)))
btnDL = Button(win, text = "↓")
btnL = Button(win, text = "<-")
btnR = Button(win, text = "->")
btnGO = Button(win, text = "OK")
btnGO2 = Button(win, text = "+")

scale = Scale(win, from_=0, to=1000, orient=HORIZONTAL,length=200, variable = scalevar, showvalue=0, command = settime)
scale2 = Scale(win, from_=200, to=0, orient=VERTICAL,length=80, variable = scalevar2, showvalue=0, command = funcSCALE)
scale2.place(x=580, y=2)
scale2.set(100)
my_label = Label(win, text = "")
my_label.place(x=5, y=36)
my_label2 = Label(win, text = "")
my_label2.place(x=220, y=66)
var = Entry(win, width=20) 
var.place(x=5, y=5)
var.focus()
var.bind('<Return>', func)
btnL.place(x=5, y=62)
btnR.place(x=60, y=62)
btnGO.place(x=115, y=62)
win.bind_all("<Button-1>", lambda event: event.widget.focus_set())
filemenu = Menu(win, tearoff=0)
filemenu.add_command(label="toggle shuffle", command=funcRAN)
filemenu.add_command(label="toggle limit duration", command=funcDur)
editmenu = Menu(menubar, tearoff=0)
editmenu.add_command(label="all results to playlist", command=funcImportall)
editmenu.add_command(label="clear playlist", command=funcClear)
menubar.add_cascade(label="Options", menu=filemenu)
menubar.add_cascade(label="Playlists", menu=editmenu)
win.config(menu=menubar)
win.bind('<space>',lambda event:funcPPTRANSFER())
win.after(2000, lambda:every_second())
var.focus()
win.mainloop()
Bondage answered 14/12, 2022 at 13:8 Comment(0)
C
0

MVP non python

This way doesn't use python but uses clitools some one might care for.

Save the file something like ~/.music.sh Then in a bashrc file source ~/.music.sh

Example command after sourcing:

play_mesh_up 75

#!/usr/bin/env zsh

# requirements:
# mvp
# youtube-dl

function play_mesh_up() {
  _VOLUME="${1:-50}"
  mpv --no-video --volume=$_VOLUME "https://youtu.be/NCZPJrH_DqQ"
}

function play_lofi_focus() {
  _VOLUME="${1:-50}"
  mpv --no-video --volume=$_VOLUME "https://www.youtube.com/watch?v=5qap5aO4i9A"
}
Cns answered 5/5, 2023 at 14:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.