204 lines
6.4 KiB
Python

import argparse, os, time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
# Driver function
def main():
# Parse CLI arguments and store in a dictionary
parser = argparse.ArgumentParser()
parser.add_argument("-host", help="Jellyfin server address with protocol and port.")
parser.add_argument("-username", help="Username.")
parser.add_argument("-password", help="Password.")
parser.add_argument("-name", help="Name of episode to search for.")
parser.add_argument(
"--tests", help="Space separated list of Selenium tests to run.", type=str, nargs="+"
)
parser.add_argument(
"--browsers",
help="Space separated list of browsers to run tests with.",
type=str,
nargs="+",
choices=["chrome", "firefox"],
)
args = parser.parse_args()
server = {
"host": args.host,
"username": args.username,
"password": args.password,
"episode": args.name,
"browsers": args.browsers,
"tests": args.tests,
}
# Print the server info for debugging and run the test
print()
print(f"Browsers: {server['browsers']}")
print(f"Address: {server['host']}")
print(f"Username: {server['username']}")
print(f"Episode: \"{server['episode']}\"")
print(f"Tests: {server['tests']}")
print()
# Setup the list of drivers to run tests with
if server["browsers"] is None:
print("[!] --browsers is required")
exit(1)
drivers = []
if "chrome" in server["browsers"]:
drivers = [("http://127.0.0.1:4444", "Chrome")]
if "firefox" in server["browsers"]:
drivers.append(("http://127.0.0.1:4445", "Firefox"))
# Test with all selected drivers
for driver in drivers:
print(f"[!] Starting new test run using {driver[1]}")
test_server(server, driver[0], driver[1])
print()
# Main server test function
def test_server(server, executor, driver_type):
# Configure Selenium to use a remote driver
print(f"[+] Configuring Selenium to use executor {executor} of type {driver_type}")
opts = None
if driver_type == "Chrome":
opts = webdriver.ChromeOptions()
elif driver_type == "Firefox":
opts = webdriver.FirefoxOptions()
else:
raise ValueError(f"Unknown driver type {driver_type}")
driver = webdriver.Remote(command_executor=executor, options=opts)
try:
# Wait up to two seconds when finding an element before reporting failure
driver.implicitly_wait(2)
# Login to Jellyfin
driver.get(make_url(server, "/"))
print(f"[+] Authenticating as {server['username']}")
login(driver, server)
if "skip_button" in server["tests"]:
# Play the user specified episode and verify skip intro button functionality. This episode is expected to:
# * already have been analyzed for an introduction
# * have an introduction at the beginning of the episode
print("[+] Testing skip intro button")
test_skip_button(driver, server)
print("[+] All tests completed successfully")
finally:
# Unconditionally end the Selenium session
driver.quit()
def login(driver, server):
# Append the Enter key to the password to submit the form
us = server["username"]
pw = server["password"] + Keys.ENTER
# Fill out and submit the login form
driver.find_element(By.ID, "txtManualName").send_keys(us)
driver.find_element(By.ID, "txtManualPassword").send_keys(pw)
def test_skip_button(driver, server):
print(f" [+] Searching for episode \"{server['episode']}\"")
search = driver.find_element(By.CSS_SELECTOR, ".headerSearchButton span.search")
if driver.capabilities["browserName"] == "firefox":
# Work around a FF bug where the search element isn't considered clickable right away
time.sleep(1)
# Click the search button
search.click()
# Type the episode name
driver.find_element(By.CSS_SELECTOR, ".searchfields-txtSearch").send_keys(
server["episode"]
)
# Click the first episode in the search results
driver.find_element(
By.CSS_SELECTOR, ".searchResults button[data-type='Episode']"
).click()
# Wait for the episode page to finish loading by searching for the episode description (overview)
driver.find_element(By.CSS_SELECTOR, ".overview")
print(f" [+] Waiting for playback to start")
# Click the play button in the toolbar
driver.find_element(
By.CSS_SELECTOR, "div.mainDetailButtons span.play_arrow"
).click()
# Wait for playback to start by searching for the lower OSD control bar
driver.find_element(By.CSS_SELECTOR, ".osdControls")
# Let the video play a little bit so the position before clicking the button can be logged
print(" [+] Playing video")
time.sleep(2)
screenshot(driver, "skip_button_pre_skip")
assert_video_playing(driver)
# Find the skip intro button and click it, logging the new video position after the seek is preformed
print(" [+] Clicking skip intro button")
driver.find_element(By.CSS_SELECTOR, "div#skipIntro").click()
time.sleep(1)
screenshot(driver, "skip_button_post_skip")
assert_video_playing(driver)
# Keep playing the video for a few seconds to ensure that:
# * the intro was successfully skipped
# * video playback continued automatically post button click
print(" [+] Verifying post skip position")
time.sleep(4)
screenshot(driver, "skip_button_post_play")
assert_video_playing(driver)
# Utility functions
def make_url(server, url):
final = server["host"] + url
print(f"[+] Navigating to {final}")
return final
def screenshot(driver, filename):
dest = f"screenshots/{filename}.png"
driver.save_screenshot(dest)
# Returns the current video playback position and if the video is paused.
# Will raise an exception if playback is paused as the video shouldn't ever pause when using this plugin.
def assert_video_playing(driver):
ret = driver.execute_script(
"""
const video = document.querySelector("video");
return {
"position": video.currentTime,
"paused": video.paused
};
"""
)
if ret["paused"]:
raise Exception("Video should not be paused")
print(f" [+] Video playback position: {ret['position']}")
return ret
main()