#!/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 "<b>VLC Song Information Query</b>" + \
		"<hr>" + \
		"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 <b>(required)</b>." + \
		"<br/><br/>" + \
		"<b>Parameters</b>" + \
		"<hr>" + \
		"<u>- Text Source</u>: Select from the already created Text Sources where the song information will be displayed." + \
		"<br/>" + \
		"<u>- Song String</u>: 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." + \
		"<br/>" + \
		" Common tags: <code>%artist</code>, <code>%title</code>, <code>%album</code>, <code>%filename</code>, <code>%date</code>" + \
		"<br/>" + \
		"<u>- Image Source</u>: Select from the already created Image Sources where the song cover art will be displayed." + \
		"<br/>" + \
		"<u>- HTTP Username and HTTP Password Source</u>: VLC login information. Input the values you used when you configured VLC." + \
		"<br/>" + \
		"<u>- HTTP URL</u>: URL of VLC's status information used to gather the song information. Default value is http://localhost:8080/requests/status.json." + \
		"<br/>" + \
		"<u>- Update Interval</u>: Specify how often the script should check for a song change. Increase this value if you are having performance problems." + \
		"<br/><br/>" + \
		"Dagnus' rewrite of Hawezo's \"VLC Current Song\" script." + \
		"<br/>" + \
		"<hr>"


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
