#!/usr/bin/env python
# title : vlc_song_info.py
# description : VLC Song Information Query allows OBS to query VLC currently playing song
# : and update the scene accordingly.
# author : Dagnus
# version : 20190610
# dependencies : - Python 3.6 (https://www.python.org/)
# : - requests (http://www.python-requests.org/)
# notes : Please refer to the following page to obtain installation instructions
# known issues : 1. There is a performance problem when VLC is not opened and the script tries
# : to connect to its web interface. It will lead to the OBS interface becoming
# : unresponsive for 2-3 seconds when that happens.
# : To help make it less of a problem, the script will double its update
# : interval at each failure until it manages to connect. You can reset the
# : update interval by modifying this value in the script configuration screen.
# : If it manages to connect again, the configured update interval will be
# : restored.
# :
# python_version : 3.6+
# ==============================================================================
import obspython as obs
import requests, urllib
import re, json
import os
# Allow for the user to
def script_description():
return "VLC Song Information Query" + \
"
" + \
"Allows you to querry song information from VLC's web interface. " + \
"Please refer to the OBS script download page in order to learn how to configure VLC (required)." + \
"
" + \
"Parameters" + \
"
" + \
"- Text Source: Select from the already created Text Sources where the song information will be displayed." + \
"
" + \
"- Song String: Allows you to specify what data you want to query from VLC. The script will attempt to replace each word preceded by '%' (a tag) with information from the song's meta data. The rest of the string as well as the tags without match will be left unchanged." + \
"
" + \
" Common tags: %artist
, %title
, %album
, %filename
, %date
" + \
"
" + \
"- Image Source: Select from the already created Image Sources where the song cover art will be displayed." + \
"
" + \
"- HTTP Username and HTTP Password Source: VLC login information. Input the values you used when you configured VLC." + \
"
" + \
"- HTTP URL: URL of VLC's status information used to gather the song information. Default value is http://localhost:8080/requests/status.json." + \
"
" + \
"- Update Interval: Specify how often the script should check for a song change. Increase this value if you are having performance problems." + \
"
" + \
"Dagnus' rewrite of Hawezo's \"VLC Current Song\" script." + \
"
" + \
"
"
activated = False
textSource = ""
imageSource = ""
songString = 'Now Playing: %title - %artist'
formatingItems = None
formatingRegExp = None
httpUsername = ""
httpPassword = ""
httpUrl = "http://localhost:8080/requests/status.json"
updateInterval = 5
errorUpdateInterval = 5
previousplid = 0
debug = False
useRequestsModule = True
requestsSession = None
urlLibPool = None
def script_properties():
settings = obs.obs_properties_create()
# Create two dropdown lists with all existing Text and Images sources
textSourceList = obs.obs_properties_add_list(settings, "textSource", "Text Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
obs.obs_properties_add_text(settings, "songString", "Song String", obs.OBS_TEXT_DEFAULT )
imageSourceList = obs.obs_properties_add_list(settings, "imageSource", "Image Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
sources = obs.obs_enum_sources()
if (sources):
for source in sources:
source_id = obs.obs_source_get_id(source)
if (source_id == "text_gdiplus" or source_id == "text_ft2_source"):
name = obs.obs_source_get_name(source)
obs.obs_property_list_add_string(textSourceList, name, name)
elif (source_id == "image_source"):
name = obs.obs_source_get_name(source)
obs.obs_property_list_add_string(imageSourceList, name, name)
obs.source_list_release(sources)
obs.obs_properties_add_text(settings, "httpUsername", "HTTP Username", obs.OBS_TEXT_DEFAULT )
obs.obs_properties_add_text(settings, "httpPassword", "HTTP Password", obs.OBS_TEXT_PASSWORD )
obs.obs_properties_add_text(settings, "httpUrl", "HTTP URL", obs.OBS_TEXT_DEFAULT )
obs.obs_properties_add_int(settings, "updateInterval", "Update Interval (seconds)", 1, 3600, 1 )
obs.obs_properties_add_bool(settings, "debug", "Debug" )
return settings
def script_defaults(settings):
obs.obs_data_set_default_string(settings, "textSource", "")
obs.obs_data_set_default_string(settings, "songString", songString)
obs.obs_data_set_default_string(settings, "imageSource", "")
obs.obs_data_set_default_string(settings, "httpUsername", "")
obs.obs_data_set_default_string(settings, "httpPassword", "")
obs.obs_data_set_default_string(settings, "httpUrl", httpUrl)
obs.obs_data_set_default_int(settings, "updateInterval", updateInterval)
obs.obs_data_set_default_bool(settings, "debug", debug)
def script_update(settings):
Activate(False)
global songString
global formatingItems
global formatingRegExp
global httpUsername
global httpPassword
global httpUrl
global textSource
global imageSource
global updateInterval
global errorUpdateInterval
global debug
global previousplid
# Update global variables
songString = obs.obs_data_get_string(settings, "songString")
httpUrl = obs.obs_data_get_string(settings, "httpUrl")
textSource = obs.obs_data_get_string(settings, "textSource")
imageSource = obs.obs_data_get_string(settings, "imageSource")
updateInterval = obs.obs_data_get_int(settings, "updateInterval")
errorUpdateInterval = updateInterval
debug = obs.obs_data_get_bool(settings, "debug")
# Reinitialize previousplid to pickup any change in already running songs
previousplid = 0
# Pre-compile regex for use during string update
formatingItems = re.findall(r'%\w+', songString)
formatingRegExp = re.compile("|".join(formatingItems))
if debug: print(formatingRegExp)
# Recreate HTTP headers
httpUsername = obs.obs_data_get_string(settings, "httpUsername")
httpPassword = obs.obs_data_get_string(settings, "httpPassword")
if (useRequestsModule):
global requestsSession
requestsSession = requests.Session()
requestsSession.auth = (httpUsername, httpPassword)
activating = False
textSourceObj = obs.obs_get_source_by_name(textSource)
if (textSourceObj):
activating |= obs.obs_source_active(textSourceObj)
obs.obs_source_release(textSourceObj)
imageSourceObj = obs.obs_get_source_by_name(imageSource)
if (imageSourceObj):
activating |= obs.obs_source_active(imageSourceObj)
obs.obs_source_release(imageSourceObj)
Activate(activating)
# When the script loads, register for source activation callbacks to automatically enable/disable the update
def script_load(settings):
obsSignalHandler = obs.obs_get_signal_handler()
obs.signal_handler_connect(obsSignalHandler, "source_activate", SourceActivated)
obs.signal_handler_connect(obsSignalHandler, "source_deactivate", SourceDeactivated)
def Activate(activating):
global activated
if (activated == activating):
return
activated = activating
if (activating):
CheckAndUpdate()
obs.timer_add(CheckAndUpdate, updateInterval * 1000)
else:
obs.timer_remove(CheckAndUpdate)
def ActivateSignal(cd, activating):
source = obs.calldata_source(cd, "source")
if (source):
sourceName = obs.obs_source_get_name(source)
if (sourceName == textSource):
imageSourceObj = obs.obs_get_source_by_name(imageSource)
if (imageSourceObj):
activating |= obs.obs_source_active(imageSourceObj)
obs.obs_source_release(imageSourceObj)
Activate(activating)
elif (sourceName == imageSource):
textSourceObj = obs.obs_get_source_by_name(textSource)
if (textSourceObj):
activating |= obs.obs_source_active(textSourceObj)
obs.obs_source_release(textSourceObj)
Activate(activating)
def SourceActivated(cd):
if debug: print("SourceActivated")
ActivateSignal(cd, True)
def SourceDeactivated(cd):
if debug: print("SourceDeactivated")
ActivateSignal(cd, False)
def CheckAndUpdate():
jsonData = None
try:
response = requestsSession.get(httpUrl)
if (not response):
if debug:
print("VLC might be misconfigured or unreachable.")
return
except Exception as err:
# When this happens, OBS gets blocked for long periods of time. To aleviate this,
# double the errorUpdateInterval timer each time
global errorUpdateInterval
errorUpdateInterval *= 2
obs.remove_current_callback()
obs.timer_add(CheckAndUpdate, errorUpdateInterval * 1000)
if debug:
print(f"Error occurred while accessing {httpUrl}: {err}")
print(f"VLC might be misconfigured or unreachable. Next try in {errorUpdateInterval} seconds")
return
# We were in an error state but managed to escape it. Restore the normal update interval
if (errorUpdateInterval != updateInterval):
obs.remove_current_callback()
obs.timer_add(CheckAndUpdate, updateInterval * 1000)
global previousplid
try:
jsonData = response.json()
# Do not try to update the currently playing song, return early
currentplid = jsonData["currentplid"]
if (previousplid == currentplid):
return
else:
previousplid = currentplid
outDict = dict()
for formatingItem in formatingItems:
outDict[formatingItem] = jsonData["information"]["category"]["meta"].get(formatingItem[1:], formatingItem)
finalizedString = formatingRegExp.sub(lambda m: outDict[m.group(0)], songString)
# Update Text Source
source = obs.obs_get_source_by_name(textSource)
if (source):
settings = obs.obs_data_create()
obs.obs_data_set_string(settings, "text", finalizedString)
obs.obs_source_update(source, settings)
obs.obs_data_release(settings)
obs.obs_source_release(source)
# Update Image Source
source = obs.obs_get_source_by_name(imageSource)
if (source):
settings = obs.obs_data_create()
url_data = urllib.parse.urlparse(jsonData["information"]["category"]["meta"].get("artwork_url", "NO_ARTWORK"))
imgPath = urllib.parse.unquote(url_data.path)
if os.name == 'nt':
# For some reason, urlparse is leaving us a leading '/'
# Probably fine on Linux, but needs to be removed on Windows
imgPath = imgPath[1:]
obs.obs_data_set_string(settings, "file", imgPath)
obs.obs_source_update(source, settings)
obs.obs_data_release(settings)
obs.obs_source_release(source)
except:
# In case something happened, try to reset previousplid to fetch new data before exiting the function
previousplid = 0
return