diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 650abf76..35fbf409 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -84,6 +84,7 @@ jobs: "-xr!plexhints*" \ "-xr!Themerr-plex.bundle/.*" \ "-xr!Themerr-plex.bundle/cache.sqlite" \ + "-xr!Themerr-plex.bundle/codecov.yml" \ "-xr!Themerr-plex.bundle/crowdin.yml" \ "-xr!Themerr-plex.bundle/DOCKER_README.md" \ "-xr!Themerr-plex.bundle/Dockerfile" \ @@ -120,8 +121,6 @@ jobs: os: [windows-latest, ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} - env: - PLEXAPI_AUTH_SERVER_BASEURL: http://127.0.0.1:32400 steps: - name: Checkout uses: actions/checkout@v4 @@ -146,6 +145,21 @@ jobs: - name: Set up Python uses: LizardByte/.github/actions/setup_python2@nightly + - name: Bootstrap Plex server + env: + PLEXAPI_PLEXAPI_TIMEOUT: "60" + id: bootstrap + uses: LizardByte/plexhints@v0.1.0 + with: + additional_server_queries_put: >- + /system/agents/com.plexapp.agents.imdb/config/1?order=com.plexapp.agents.imdb%2Cdev.lizardbyte.themerr-plex + /system/agents/com.plexapp.agents.themoviedb/config/1?order=com.plexapp.agents.themoviedb%2Cdev.lizardbyte.themerr-plex + plugin_bundles_to_install: >- + Themerr-plex.bundle + without_shows: true + without_music: true + without_photos: true + - name: Install python dependencies shell: bash run: | @@ -153,90 +167,37 @@ jobs: pip setuptools wheel python -m pip --no-python-version-warning --disable-pip-version-check install -r requirements-dev.txt - - name: Install Plex Media Server - shell: bash - run: | - if [[ "${{ matrix.os }}" == "windows-latest" ]]; then - choco install plexmediaserver - elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then - brew install --cask plex-media-server - - # starting with pms 1.29.2 servers must be claimed... disable that - # https://forums.plex.tv/t/new-server-claiming-requirement-for-macos/816337 - defaults write com.plexapp.plexmediaserver enableLocalSecurity -bool FALSE - - # copy plugin before starting plex server - mkdir -p "${HOME}/Library/Application Support/Plex Media Server/Plug-ins" - cp -r ./Themerr-plex.bundle "${HOME}/Library/Application Support/Plex Media Server/Plug-ins/" - - open "/Applications/Plex Media Server.app" - elif [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then - curl https://downloads.plex.tv/plex-keys/PlexSign.key | sudo apt-key add - - echo deb https://downloads.plex.tv/repo/deb public main | \ - sudo tee /etc/apt/sources.list.d/plexmediaserver.list - sudo apt-get update - sudo apt-get install plexmediaserver - - # stop service - sudo systemctl stop plexmediaserver - - # debug - cat /lib/systemd/system/plexmediaserver.service - - # do not edit service directly, use override - override=/etc/systemd/system/plexmediaserver.service.d/override.conf - sudo mkdir -p $(dirname ${override}) - sudo touch ${override} - echo "[Service]" | sudo tee ${override} - echo "User=$USER" | sudo tee -a ${override} - - # take ownership - sudo chown -R $USER:$USER "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" - - # reload service - sudo systemctl daemon-reload - - # start - sudo systemctl start plexmediaserver - else - echo "Unknown OS: ${{ matrix.os }}" - exit 1 - fi - - - name: Update Plex registry settings - if: ${{ matrix.os == 'windows-latest' }} - run: | - # starting with pmps 1.32.2 servers must be claimed... disable that - # https://forums.plex.tv/t/new-claiming-requirement-for-windows/839096 - REG ADD "HKCU\Software\Plex, Inc.\Plex Media Server" /v enableLocalSecurity /t REG_DWORD /d 0 /f - - - name: Bootstrap Plex server - id: boostrap - shell: bash - run: | - python \ - -u scripts/plex-bootstraptest.py \ - --destination plex \ - --advertise-ip 127.0.0.1 \ - --bootstrap-timeout 540 \ - --no-docker \ - --server-name plex-test-${{ matrix.os }}-${{ github.run_id }} \ - --without-shows \ - --without-music \ - --without-photos \ - --unclaimed - - name: Test with pytest + env: + PLEX_PLUGIN_LOG_PATH: ${{ steps.bootstrap.outputs.PLEX_PLUGIN_LOG_PATH }} + PLEXAPI_AUTH_SERVER_BASEURL: ${{ steps.bootstrap.outputs.PLEX_SERVER_BASEURL }} + PLEXAPI_AUTH_SERVER_TOKEN: ${{ steps.bootstrap.outputs.PLEXTOKEN }} + PLEXAPI_PLEXAPI_TIMEOUT: "60" + PLEXTOKEN: ${{ steps.bootstrap.outputs.PLEXTOKEN }} id: test shell: bash run: | python -m pytest \ - -rXs \ + -rxXs \ + --maxfail=1 \ --tb=native \ --verbose \ --cov=Contents/Code \ tests + - name: Debug log file + if: always() + shell: bash + run: | + echo "Debugging log file" + if [[ "${{ runner.os }}" == "Windows" ]]; then + log_file=$(cygpath.exe -u \ + "${{ steps.bootstrap.outputs.PLEX_PLUGIN_LOG_PATH }}/dev.lizardbyte.themerr-plex.log") + else + log_file="${{ steps.bootstrap.outputs.PLEX_PLUGIN_LOG_PATH }}/dev.lizardbyte.themerr-plex.log" + fi + cat "${log_file}" + - name: Upload coverage # any except cancelled or skipped if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') diff --git a/Contents/Code/scheduled_tasks.py b/Contents/Code/scheduled_tasks.py index 54ae31f7..9e6d8e18 100644 --- a/Contents/Code/scheduled_tasks.py +++ b/Contents/Code/scheduled_tasks.py @@ -36,7 +36,7 @@ def run_threaded(target, daemon=None, args=(), **kwargs): - # type: (Callable, Optional[bool], Iterable, Mapping[str, Any]) -> None + # type: (Callable, Optional[bool], Iterable, Mapping[str, Any]) -> threading.Thread """ Run a function in a thread. @@ -54,6 +54,11 @@ def run_threaded(target, daemon=None, args=(), **kwargs): kwargs : Mapping[str, Any] The keyword arguments to pass to the function. + Returns + ------- + threading.Thread + The thread that the function is running in. + Examples -------- >>> run_threaded(target=Log.Info, daemon=True, args=['Hello, world!']) @@ -63,6 +68,7 @@ def run_threaded(target, daemon=None, args=(), **kwargs): if daemon: job_thread.daemon = True job_thread.start() + return job_thread def schedule_loop(): @@ -77,6 +83,7 @@ def schedule_loop(): >>> schedule_loop() ... """ + time.sleep(60) # give a little time for the server to start schedule.run_all() # run all jobs once while True: diff --git a/requirements-dev.txt b/requirements-dev.txt index 0faabf42..9dc01391 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,11 +2,10 @@ flake8==3.9.2;python_version<"3" m2r2==0.3.2;python_version<"3" numpydoc==0.9.2;python_version<"3" -git+https://github.com/LizardByte/plexhints.git#egg=plexhints # type hinting library for plex development +plexhints==0.1.0 # type hinting library for plex development plexapi-backport[alert]==4.15.2 pytest==4.6.11;python_version<"3" pytest-cov==2.12.1;python_version<"3" rstcheck==3.5.0;python_version<"3" Sphinx==1.8.6;python_version<"3" sphinx-rtd-theme==1.2.0;python_version<"3" -tqdm==4.64.1;python_version<"3" diff --git a/scripts/plex-bootstraptest.py b/scripts/plex-bootstraptest.py deleted file mode 100644 index 0d09da31..00000000 --- a/scripts/plex-bootstraptest.py +++ /dev/null @@ -1,712 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The script is used to bootstrap a the test environment for plexapi -with all the libraries required for testing. - -By default this uses a docker. - -It can be used manually using: -python plex-bootraptest.py --no-docker --server-name name_of_server --account Hellowlol --password yourpassword - -Borrowed from python-plexapi and python-plexapi-backport... then modified. -""" -from __future__ import absolute_import -from __future__ import division -from builtins import dict -from builtins import input -from builtins import range -import argparse -import os -import platform -import shutil -import socket -import time -from plexapi.backports import glob -from plexapi.backports import makedirs -from shutil import copyfile -try: - from shutil import which -except ImportError: - from backports.shutil_which import which -import subprocess -from uuid import uuid4 - -import plexapi -from plexapi.exceptions import BadRequest, NotFound -from plexapi.myplex import MyPlexAccount -from plexapi.server import PlexServer -from plexapi.utils import SEARCHTYPES -from tqdm import tqdm - -DOCKER_CMD = [ - "docker", - "run", - "-d", - "--name", - "plex-test-%(container_name_extra)s%(image_tag)s", - "--restart", - "on-failure", - "-p", - "32400:32400/tcp", - "-p", - "3005:3005/tcp", - "-p", - "8324:8324/tcp", - "-p", - "32469:32469/tcp", - "-p", - "1900:1900/udp", - "-p", - "32410:32410/udp", - "-p", - "32412:32412/udp", - "-p", - "32413:32413/udp", - "-p", - "32414:32414/udp", - "-e", - "PLEX_CLAIM=%(claim_token)s", - "-e", - "ADVERTISE_IP=http://%(advertise_ip)s:32400/", - "-e", - "TZ=%(timezone)s", - "-e", - "LANG=%(language)s", - "-h", - "%(hostname)s", - "-v", - "%(destination)s/db:/config", - "-v", - "%(destination)s/transcode:/transcode", - "-v", - "%(destination)s/media:/data", - "plexinc/pms-docker:%(image_tag)s", -] - - -BASE_DIR_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -STUB_MOVIE_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "video_stub.mp4") -STUB_MP3_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "audio_stub.mp3") -STUB_IMAGE_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "cute_cat.jpg") - - -def check_ext(path, ext): - """I hate glob so much.""" - result = [] - for root, dirs, fil in os.walk(path): - for f in fil: - fp = os.path.join(root, f) - if fp.endswith(ext): - result.append(fp) - - return result - - -class ExistingSection(Exception): - """This server has sections, exiting""" - - def __init__(self, *args): - raise SystemExit("This server has sections exiting") - - -def clean_pms(server, path): - for section in server.library.sections(): - print("Deleting %s" % section.title) - section.delete() - - server.library.cleanBundles() - server.library.optimize() - print("optimized db and removed any bundles") - - shutil.rmtree(path, ignore_errors=False, onerror=None) - print("Deleted %s" % path) - - -def setup_music(music_path, docker=False): - print("Setup files for the Music section..") - makedirs(music_path, exist_ok=True) - - all_music = { - - "Broke for free": { - "Layers": [ - "1 - As Colorful As Ever.mp3", - # "02 - Knock Knock.mp3", - # "03 - Only Knows.mp3", - # "04 - If.mp3", - # "05 - Note Drop.mp3", - # "06 - Murmur.mp3", - # "07 - Spellbound.mp3", - # "08 - The Collector.mp3", - # "09 - Quit Bitching.mp3", - # "10 - A Year.mp3", - ] - }, - - } - - m3u_file = open(os.path.join(music_path, "playlist.m3u"), "w") - - for artist, album in all_music.items(): - for k, v in album.items(): - artist_album = os.path.join(music_path, artist, k) - makedirs(artist_album, exist_ok=True) - for song in v: - trackpath = os.path.join(artist_album, song) - copyfile(STUB_MP3_PATH, trackpath) - - if docker: - reltrackpath = os.path.relpath(trackpath, os.path.dirname(music_path)) - m3u_file.write(os.path.join("/data", reltrackpath) + "\n") - else: - m3u_file.write(trackpath + "\n") - - m3u_file.close() - - return len(check_ext(music_path, (".mp3"))) - - -def setup_movies(movies_path): - print("Setup files for the Movies section..") - makedirs(movies_path, exist_ok=True) - - if len(glob(movies_path + "/*.mkv", recursive=True)) == 4: - return 4 - - required_movies = { - "Elephants Dream": 2006, - "Sita Sings the Blues": 2008, - "Big Buck Bunny": 2008, - "Sintel": 2010, - } - expected_media_count = 0 - for name, year in required_movies.items(): - expected_media_count += 1 - if not os.path.isfile(get_movie_path(movies_path, name, year)): - copyfile(STUB_MOVIE_PATH, get_movie_path(movies_path, name, year)) - - return expected_media_count - - -def setup_images(photos_path): - print("Setup files for the Photos section..") - - makedirs(photos_path, exist_ok=True) - # expected_photo_count = 0 - folders = { - ("Cats",): 3, - ("Cats", "Cats in bed"): 7, - ("Cats", "Cats not in bed"): 1, - ("Cats", "Not cats in bed"): 1, - } - has_photos = 0 - for folder_path, required_cnt in folders.items(): - folder_path = os.path.join(photos_path, *folder_path) - makedirs(folder_path, exist_ok=True) - photos_in_folder = len(glob(os.path.join(folder_path, "/*.jpg"))) - while photos_in_folder < required_cnt: - # Dunno why this is need got permission error on photo0.jpg - photos_in_folder += 1 - full_path = os.path.join(folder_path, "photo%d.jpg" % photos_in_folder) - copyfile(STUB_IMAGE_PATH, full_path) - has_photos += photos_in_folder - - return len(check_ext(photos_path, (".jpg"))) - - -def setup_show(tvshows_path): - print("Setup files for the TV-Shows section..") - makedirs(tvshows_path, exist_ok=True) - makedirs(os.path.join(tvshows_path, "Game of Thrones"), exist_ok=True) - makedirs(os.path.join(tvshows_path, "The 100"), exist_ok=True) - required_tv_shows = { - "Game of Thrones": [list(range(1, 11)), list(range(1, 11))], - "The 100": [list(range(1, 14)), list(range(1, 17))], - } - expected_media_count = 0 - for show_name, seasons in required_tv_shows.items(): - for season_id, episodes in enumerate(seasons, start=1): - for episode_id in episodes: - expected_media_count += 1 - episode_path = get_tvshow_path( - tvshows_path, show_name, season_id, episode_id - ) - if not os.path.isfile(episode_path): - copyfile(STUB_MOVIE_PATH, episode_path) - - return expected_media_count - - -def get_default_ip(): - """ Return the first IP address of the current machine if available. """ - available_ips = list( - set( - [ - i[4][0] - for i in socket.getaddrinfo(socket.gethostname(), None) - if i[4][0] not in ("127.0.0.1", "::1") - and not i[4][0].startswith("fe80:") - ] - ) - ) - return available_ips[0] if len(available_ips) else None - - -def get_plex_account(opts): - """ Authenticate with Plex using the command line options. """ - if not opts.unclaimed: - if opts.token: - return MyPlexAccount(token=opts.token) - return plexapi.utils.getMyPlexAccount(opts) - return None - - -def get_movie_path(movies_path, name, year): - """ Return a movie path given its title and year. """ - return os.path.join(movies_path, "%s (%d).mp4" % (name, year)) - - -def get_tvshow_path(tvshows_path, name, season, episode): - """ Return a TV show path given its title, season, and episode. """ - return os.path.join(tvshows_path, name, "S%02dE%02d.mp4" % (season, episode)) - - -def add_library_section(server, section): - """ Add the specified section to our Plex instance. This tends to be a bit - flaky, so we retry a few times here. - """ - start = time.time() - runtime = 0 - while runtime < 60: - try: - server.library.add(**section) - return True - except BadRequest as err: - if "server is still starting up. Please retry later" in str(err): - time.sleep(1) - continue - raise - runtime = time.time() - start - raise SystemExit("Timeout adding section to Plex instance.") - - -def create_section(server, section, opts): # noqa: C901 - processed_media = 0 - expected_media_count = section.pop("expected_media_count", 0) - expected_media_type = (section["type"],) - if section["type"] == "show": - expected_media_type = ("show", "season", "episode") - if section["type"] == "artist": - expected_media_type = ("artist", "album", "track") - expected_media_type = tuple(SEARCHTYPES[t] for t in expected_media_type) - - def alert_callback(data): - """ Listen to the Plex notifier to determine when metadata scanning is complete. """ - global processed_media - if data["type"] == "timeline": - for entry in data["TimelineEntry"]: - if ( - entry.get("identifier", "com.plexapp.plugins.library") - == "com.plexapp.plugins.library" - ): - # Missed mediaState means that media was processed (analyzed & thumbnailed) - if ( - "mediaState" not in entry - and entry["type"] in expected_media_type - ): - # state=5 means record processed, applicable only when metadata source was set - if entry["state"] == 5: - cnt = 1 - if entry["type"] == SEARCHTYPES["show"]: - show = server.library.sectionByID( - entry["sectionID"] - ).get(entry["title"]) - cnt = show.leafCount - bar.update(cnt) - processed_media += cnt - # state=1 means record processed, when no metadata source was set - elif ( - entry["state"] == 1 - and entry["type"] == SEARCHTYPES["photo"] - ): - bar.update() - processed_media += 1 - - runtime = 0 - start = time.time() - bar = tqdm(desc="Scanning section " + section["name"], total=expected_media_count) - notifier = server.startAlertListener(alert_callback) - time.sleep(3) - add_library_section(server, section) - while bar.n < bar.total: - if runtime >= 120: - print("Metadata scan taking too long, but will continue anyway..") - break - time.sleep(3) - runtime = int(time.time() - start) - bar.close() - notifier.stop() - - -if __name__ == "__main__": # noqa: C901 - default_ip = get_default_ip() - parser = argparse.ArgumentParser(description=__doc__) - # Authentication arguments - mg = parser.add_mutually_exclusive_group() - g = mg.add_argument_group() - g.add_argument("--username", help="Your Plex username") - g.add_argument("--password", help="Your Plex password") - mg.add_argument( - "--token", - help="Plex.tv authentication token", - default=plexapi.CONFIG.get("auth.server_token"), - ) - mg.add_argument( - "--unclaimed", - help="Do not claim the server", - default=False, - action="store_true", - ) - # Test environment arguments - parser.add_argument( - "--no-docker", help="Use docker", default=False, action="store_true" - ) - parser.add_argument( - "--timezone", help="Timezone to set inside plex", default="UTC" - ) # noqa - parser.add_argument( - "--language", help="Language to set inside plex", default="en_US.UTF-8" - ) # noqa - parser.add_argument( - "--destination", - help="Local path where to store all the media", - default=os.path.join(os.getcwd(), "plex"), - ) # noqa - parser.add_argument( - "--plugin-bundle-destination", - help="Local path where to copy the plugin bundle to", - default="auto" - ) # noqa - parser.add_argument( - "--plugin-bundle-source", - help="Local path where bundle is located", - default=os.path.join(os.getcwd(), "Themerr-plex.bundle"), - ) # noqa - parser.add_argument( - "--advertise-ip", - help="IP address which should be advertised by new Plex instance", - required=default_ip is None, - default=default_ip, - ) # noqa - parser.add_argument( - "--docker-tag", help="Docker image tag to install", default="latest" - ) # noqa - parser.add_argument( - "--bootstrap-timeout", - help="Timeout for each step of bootstrap, in seconds (default: %(default)s)", - default=180, - type=int, - ) # noqa - parser.add_argument( - "--server-name", - help="Name for the new server", - default="plex-test-docker-%s" % str(uuid4()), - ) # noqa - parser.add_argument( - "--accept-eula", help="Accept Plex's EULA", default=False, action="store_true" - ) # noqa - parser.add_argument( - "--without-movies", - help="Do not create Movies section", - default=True, - dest="with_movies", - action="store_false", - ) # noqa - parser.add_argument( - "--without-shows", - help="Do not create TV Shows section", - default=True, - dest="with_shows", - action="store_false", - ) # noqa - parser.add_argument( - "--without-music", - help="Do not create Music section", - default=True, - dest="with_music", - action="store_false", - ) # noqa - parser.add_argument( - "--without-photos", - help="Do not create Photos section", - default=True, - dest="with_photos", - action="store_false", - ) # noqa - parser.add_argument( - "--show-token", - help="Display access token after bootstrap", - default=False, - action="store_true", - ) # noqa - opts, _ = parser.parse_known_args() - - account = get_plex_account(opts) - path = os.path.realpath(os.path.expanduser(opts.destination)) - media_path = os.path.join(path, "media") - makedirs(media_path, exist_ok=True) - - APP_DATA_PATH = dict( - Darwin="{}/Library/Application Support/Plex Media Server".format(os.getenv('HOME')), - Linux="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server", - Windows="{}\\Plex Media Server".format(os.getenv('LOCALAPPDATA')) - ) - - # copy the plugin - if opts.plugin_bundle_destination == "auto": - opts.plugin_bundle_destination = os.path.join( - path, "db", "Library", "Application Support", "Plex Media Server", "Plug-ins", "Themerr-plex.bundle") if \ - opts.no_docker is False else os.path.join( - APP_DATA_PATH[platform.system()], "Plug-ins", "Themerr-plex.bundle") - - try: - shutil.copytree(opts.plugin_bundle_source, opts.plugin_bundle_destination) - except OSError as e: - if 'file exists' in str(e).lower(): - print("Warning: Skipping copy, plugin already exists at %s" % opts.plugin_bundle_destination) - else: - print("Warning: %s" % e) - else: - print("Copied plugin bundle to %s" % opts.plugin_bundle_destination) - - # copy the plugin plist file back to Contents - # this is necessary to get elevated policy with plexhints - try: - shutil.copyfile(os.path.join(opts.plugin_bundle_source, "Contents", "Info.plist"), - os.path.join(os.getcwd(), "Contents", "Info.plist")) - except OSError as e: - if 'file exists' in str(e).lower(): - print("Warning: Skipping copy, plugin plist already exists at %s" % os.path.join( - os.getcwd(), "Contents", "Info.plist")) - else: - print("Warning: %s" % e) - else: - print("Copied plugin plist to %s" % os.path.join(os.getcwd(), "Contents", "Info.plist")) - - # Download the Plex Docker image - if opts.no_docker is False: - print( - "Creating Plex instance named %s with advertised ip %s" - % (opts.server_name, opts.advertise_ip) - ) - if which("docker") is None: - print("Docker is required to be available") - exit(1) - if subprocess.call(["docker", "pull", "plexinc/pms-docker:%s" % opts.docker_tag]) != 0: - print("Got an error when executing docker pull!") - exit(1) - - # Start the Plex Docker container - - arg_bindings = { - "destination": path, - "hostname": opts.server_name, - "claim_token": account.claimToken() if account else "", - "timezone": opts.timezone, - "language": opts.language, - "advertise_ip": opts.advertise_ip, - "image_tag": opts.docker_tag, - "container_name_extra": "" if account else "unclaimed-", - } - docker_cmd = [c % arg_bindings for c in DOCKER_CMD] - exit_code = subprocess.call(docker_cmd) - if exit_code != 0: - raise SystemExit( - "Error %s while starting the Plex docker container" % exit_code - ) - - # Wait for the Plex container to start - print("Waiting for the Plex to start..") - start = time.time() - runtime = 0 - server = None - while not server and (runtime < opts.bootstrap_timeout): - try: - if account: - server = account.device(opts.server_name).connect() - else: - server = PlexServer("http://%s:32400" % opts.advertise_ip) - - except KeyboardInterrupt: - break - - except Exception as err: - print(err) - time.sleep(1) - - runtime = time.time() - start - - if not server: - raise SystemExit( - "Server didn't appear in your account after %ss" % opts.bootstrap_timeout - ) - - print("Plex container started after %ss" % int(runtime)) - print("Plex server version %s" % server.version) - - if opts.accept_eula: - server.settings.get("acceptedEULA").set(True) - # Disable settings for background tasks when using the test server. - # These tasks won't work on the test server since we are using fake media files - if not opts.unclaimed and account and account.subscriptionActive: - server.settings.get("GenerateIntroMarkerBehavior").set("never") - server.settings.get("GenerateCreditsMarkerBehavior").set("never") - server.settings.get("GenerateBIFBehavior").set("never") - server.settings.get("GenerateChapterThumbBehavior").set("never") - server.settings.get("LoudnessAnalysisBehavior").set("never") - server.settings.save() - - sections = [] - - # Lets add a check here do somebody don't mess up - # there normal server if they run manual tests. - # Like i did.... - if len(server.library.sections()) and opts.no_docker is True: - ans = input( - "The server has %s sections, do you wish to remove it?\n> " - % len(server.library.sections()) - ) - if ans in ("y", "Y", "Yes"): - ans = input( - "Are you really sure you want to delete %s libraries? There is no way back\n> " - % len(server.library.sections()) - ) - if ans in ("y", "Y", "Yes"): - clean_pms(server, path) - else: - raise ExistingSection() - else: - raise ExistingSection() - - # Prepare Movies section - if opts.with_movies: - movies_path = os.path.join(media_path, "Movies") - num_movies = setup_movies(movies_path) - - sections.append( - dict( - name="Movies-new-agent", - type="movie", - location="/data/Movies" if opts.no_docker is False else movies_path, - agent="tv.plex.agents.movie", - scanner="Plex Movie", - language="en-US", - expected_media_count=num_movies, - ) - ) - sections.append( - dict( - name="Movies-imdb-agent", - type="movie", - location="/data/Movies" if opts.no_docker is False else movies_path, - agent="com.plexapp.agents.imdb", - scanner="Plex Movie Scanner", - # language="en-US", - expected_media_count=num_movies, - ) - ) - sections.append( - dict( - name="Movies-themoviedb-agent", - type="movie", - location="/data/Movies" if opts.no_docker is False else movies_path, - agent="com.plexapp.agents.themoviedb", - scanner="Plex Movie Scanner", - # language="en-US", - expected_media_count=num_movies, - ) - ) - - # Prepare TV Show section - if opts.with_shows: - tvshows_path = os.path.join(media_path, "TV-Shows") - num_ep = setup_show(tvshows_path) - - sections.append( - dict( - name="TV Shows", - type="show", - location="/data/TV-Shows" if opts.no_docker is False else tvshows_path, - agent="tv.plex.agents.series", - scanner="Plex TV Series", - language="en-US", - expected_media_count=num_ep, - ) - ) - - # Prepare Music section - if opts.with_music: - music_path = os.path.join(media_path, "Music") - song_c = setup_music(music_path, docker=not opts.no_docker) - - sections.append( - dict( - name="Music", - type="artist", - location="/data/Music" if opts.no_docker is False else music_path, - agent="tv.plex.agents.music", - scanner="Plex Music", - language="en-US", - expected_media_count=song_c, - ) - ) - - # Prepare Photos section - if opts.with_photos: - photos_path = os.path.join(media_path, "Photos") - has_photos = setup_images(photos_path) - - sections.append( - dict( - name="Photos", - type="photo", - location="/data/Photos" if opts.no_docker is False else photos_path, - agent="com.plexapp.agents.none", - scanner="Plex Photo Scanner", - expected_media_count=has_photos, - ) - ) - - # enable plugin for supported agents - legacy_agents = [ - 'com.plexapp.agents.imdb', - 'com.plexapp.agents.themoviedb', - ] - for agent in legacy_agents: - server.query( - key='/system/agents/{agent}/config/1?order={agent}%2C{plugin_agent}'.format( - agent=agent, - plugin_agent='dev.lizardbyte.themerr-plex', - ), method=server._session.put) - - # Create the Plex library in our instance - if sections: - print("Creating the Plex libraries on %s" % server.friendlyName) - for section in sections: - create_section(server, section, opts) - - # Share this instance with the specified username - if account: - shared_username = os.environ.get("SHARED_USERNAME", "PKKid") - try: - user = account.user(shared_username) - account.updateFriend(user, server) - print("The server was shared with user %s" % shared_username) - except NotFound: - pass - - # Finished: Display our Plex details - print("Base URL is %s" % server.url("", False)) - if account and opts.show_token: - print("Auth token is %s" % account.authenticationToken) - print("Server %s is ready to use!" % opts.server_name) diff --git a/tests/conftest.py b/tests/conftest.py index 3fccc57c..d1950182 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,10 +8,8 @@ # lib imports import plexapi from plexapi.exceptions import NotFound -from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer from plexhints.agent_kit import Agent -from plexhints.core_kit import PLUGIN_LOGS_PATH import pytest import requests @@ -28,45 +26,8 @@ # plex server setup SERVER_BASEURL = plexapi.CONFIG.get("auth.server_baseurl") -MYPLEX_USERNAME = plexapi.CONFIG.get("auth.myplex_username") -MYPLEX_PASSWORD = plexapi.CONFIG.get("auth.myplex_password") SERVER_TOKEN = plexapi.CONFIG.get("auth.server_token") -TEST_AUTHENTICATED = "authenticated" -TEST_ANONYMOUSLY = "anonymously" -ANON_PARAM = pytest.param(TEST_ANONYMOUSLY, marks=pytest.mark.anonymous) -AUTH_PARAM = pytest.param(TEST_AUTHENTICATED, marks=pytest.mark.authenticated) - - -BASE_DIR_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -def pytest_generate_tests(metafunc): - if "plex" in metafunc.fixturenames: - if ( - "account" in metafunc.fixturenames - or TEST_AUTHENTICATED in metafunc.definition.keywords - ): - metafunc.parametrize("plex", [AUTH_PARAM], indirect=True) - else: - metafunc.parametrize("plex", [ANON_PARAM, AUTH_PARAM], indirect=True) - elif "account" in metafunc.fixturenames: - metafunc.parametrize("account", [AUTH_PARAM], indirect=True) - - -def pytest_runtest_setup(item): - if "client" in item.keywords and not item.config.getvalue("client"): - return pytest.skip("Need --client option to run.") - if TEST_AUTHENTICATED in item.keywords and not (MYPLEX_USERNAME and MYPLEX_PASSWORD or SERVER_TOKEN): - return pytest.skip( - "You have to specify MYPLEX_USERNAME and MYPLEX_PASSWORD or SERVER_TOKEN to run authenticated tests" - ) - if TEST_ANONYMOUSLY in item.keywords and (MYPLEX_USERNAME and MYPLEX_PASSWORD or SERVER_TOKEN): - return pytest.skip( - "Anonymous tests should be ran on unclaimed server, without providing MYPLEX_USERNAME and " - "MYPLEX_PASSWORD or SERVER_TOKEN" - ) - def wait_for_themes(movies): # ensure library is not refreshing @@ -89,6 +50,9 @@ def wait_for_themes(movies): time.sleep(3) timer += 3 + assert with_themes == total, ( + "Not all themes were uploaded in time, themes uploaded: {}/{}".format(with_themes, total)) + # basic fixtures @pytest.fixture @@ -116,7 +80,7 @@ def test_client(scope='function'): @pytest.fixture(scope="session") def plugin_logs(): # list contents of the plugin logs directory - plugin_logs = os.listdir(PLUGIN_LOGS_PATH) + plugin_logs = os.listdir(os.environ['PLEX_PLUGIN_LOG_PATH']) yield plugin_logs @@ -125,7 +89,7 @@ def plugin_logs(): @pytest.fixture(scope="session") def plugin_log_file(): # the primary plugin log file - plugin_log_file = os.path.join(PLUGIN_LOGS_PATH, "{}.log".format(constants.plugin_identifier)) + plugin_log_file = os.path.join(os.environ['PLEX_PLUGIN_LOG_PATH'], "{}.log".format(constants.plugin_identifier)) yield plugin_log_file @@ -141,35 +105,31 @@ def sess(): def plex(request, sess): assert SERVER_BASEURL, "Required SERVER_BASEURL not specified." - if request.param == TEST_AUTHENTICATED: - token = MyPlexAccount(session=sess).authenticationToken - else: - token = None - return PlexServer(SERVER_BASEURL, token, session=sess) + return PlexServer(SERVER_BASEURL, SERVER_TOKEN, session=sess) -@pytest.fixture() +@pytest.fixture(scope="session") def movies_new_agent(plex): - movies = plex.library.section("Movies-new-agent") + movies = plex.library.section("Movies") wait_for_themes(movies=movies) return movies -@pytest.fixture() +@pytest.fixture(scope="session") def movies_imdb_agent(plex): - movies = plex.library.section("Movies-imdb-agent") + movies = plex.library.section("Movies-imdb") wait_for_themes(movies=movies) return movies -@pytest.fixture() +@pytest.fixture(scope="session") def movies_themoviedb_agent(plex): - movies = plex.library.section("Movies-themoviedb-agent") + movies = plex.library.section("Movies-tmdb") wait_for_themes(movies=movies) return movies -@pytest.fixture() +@pytest.fixture(scope="session") def collection_new_agent(plex, movies_new_agent, movie_new_agent): try: return movies_new_agent.collection("Test Collection") @@ -181,7 +141,7 @@ def collection_new_agent(plex, movies_new_agent, movie_new_agent): ) -@pytest.fixture() +@pytest.fixture(scope="session") def collection_imdb_agent(plex, movies_imdb_agent, movie_imdb_agent): try: return movies_imdb_agent.collection("Test Collection") @@ -193,7 +153,7 @@ def collection_imdb_agent(plex, movies_imdb_agent, movie_imdb_agent): ) -@pytest.fixture() +@pytest.fixture(scope="session") def collection_themoviedb_agent(plex, movies_themoviedb_agent, movie_themoviedb_agent): try: return movies_themoviedb_agent.collection("Test Collection") diff --git a/tests/functional/test_plex_plugin.py b/tests/functional/test_plex_plugin.py index aac4f0d3..8c2d213c 100644 --- a/tests/functional/test_plex_plugin.py +++ b/tests/functional/test_plex_plugin.py @@ -2,9 +2,6 @@ # standard imports import os -# lib imports -import pytest - def _check_themes(movies): # ensure all movies have themes @@ -40,16 +37,13 @@ def test_plugin_log_file_exceptions(plugin_log_file): "Unexpected exception: {}".format(exception)) -@pytest.mark.anonymous def test_movies_new_agent(movies_new_agent): _check_themes(movies_new_agent) -@pytest.mark.anonymous def test_movies_imdb_agent(movies_imdb_agent): _check_themes(movies_imdb_agent) -@pytest.mark.anonymous def test_movies_themoviedb_agent(movies_themoviedb_agent): _check_themes(movies_themoviedb_agent) diff --git a/tests/unit/test_general_helper.py b/tests/unit/test_general_helper.py index 9b2a2235..7f044d78 100644 --- a/tests/unit/test_general_helper.py +++ b/tests/unit/test_general_helper.py @@ -11,7 +11,6 @@ from Code import general_helper -@pytest.mark.anonymous def test_get_media_upload_path(movies_themoviedb_agent): test_items = [ movies_themoviedb_agent.all()[0] @@ -28,7 +27,6 @@ def test_get_media_upload_path(movies_themoviedb_agent): assert os.path.isdir(media_upload_path) -@pytest.mark.anonymous def test_get_media_upload_path_invalid(movies_themoviedb_agent): test_items = [ movies_themoviedb_agent.all()[0] @@ -38,7 +36,6 @@ def test_get_media_upload_path_invalid(movies_themoviedb_agent): general_helper.get_media_upload_path(item=test_items[0], media_type='invalid') -@pytest.mark.anonymous def test_get_themerr_json_path(movies_themoviedb_agent): test_items = [ movies_themoviedb_agent.all()[0] @@ -51,7 +48,6 @@ def test_get_themerr_json_path(movies_themoviedb_agent): 'DataItems') in themerr_json_path -@pytest.mark.anonymous def test_get_themerr_json_data(movies_themoviedb_agent): test_items = [ movies_themoviedb_agent.all()[0] @@ -72,7 +68,6 @@ def test_get_themerr_settings_hash(): assert len(themerr_settings_hash) == 64 -@pytest.mark.anonymous def test_remove_uploaded_media(movies_themoviedb_agent): test_items = [ movies_themoviedb_agent.all()[0] @@ -103,7 +98,6 @@ def test_remove_uploaded_media_error_handler(): ) -@pytest.mark.anonymous def test_update_themerr_data_file(movies_themoviedb_agent): test_items = [ movies_themoviedb_agent.all()[0] diff --git a/tests/unit/test_scheduled_tasks.py b/tests/unit/test_scheduled_tasks.py new file mode 100644 index 00000000..e735bb2b --- /dev/null +++ b/tests/unit/test_scheduled_tasks.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# standard imports +import time + +# local imports +from Code import scheduled_tasks + + +def test_run_threaded(): + def hello_world(): + time.sleep(10) + return 'Hello, world!' + + test_thread = scheduled_tasks.run_threaded(target=hello_world, daemon=True) + assert test_thread.is_alive() + + test_thread.join() + assert not test_thread.is_alive() + + +def test_schedule_loop(): + test_thread = scheduled_tasks.run_threaded(target=scheduled_tasks.schedule_loop, daemon=True) + assert test_thread.is_alive() + + +def test_setup_scheduling(): + scheduled_tasks.setup_scheduling() + assert scheduled_tasks.schedule.jobs