Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CI for pytest #39

Merged
merged 17 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[report]
exclude_lines =
if TYPE_CHECKING:
36 changes: 0 additions & 36 deletions .github/workflows/code_quality_test.yml

This file was deleted.

231 changes: 231 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
name: tests

on:
push:
branches:
- master
pull_request:
branches:
- master

env:
CACHE_VERSION: 1
DEFAULT_PLEX: "claimed"
DEFAULT_PYTHON: 3.8

jobs:
code_quality_test:
name: code quality test ${{ matrix.python-version }}
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
steps:
- name: Check out code from Github
uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pylint
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Lint with pylint
run: |
# stop the build if pylint gives a score under 9
export PWD=$(pwd)
python3 -m pylint plex_auto_languages --fail-under=9 --max-line-length=127 --disable=C0114,C0115,C0116,W1203,R0903,W0238 --init-hook='import sys; sys.path.append("${PWD}")'

pytest:
name: pytest
runs-on: ubuntu-20.04
env:
PLEXAPI_AUTH_SERVER_BASEURL: http://127.0.0.1:32400
PLEX_CONTAINER: linuxserver/plex
PLEX_CONTAINER_TAG: latest
steps:
- name: Check out code from Github
uses: actions/checkout@v3

- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v3
with:
python-version: ${{ env.DEFAULT_PYTHON }}

- name: Restore Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache@v3
with:
path: venv
key: ${{ env.CACHE_VERSION }}-${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}

- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
pip install -U pip
pip install -r requirements.txt
pip install -r tests/requirements.txt

- name: Get PMS Docker image digest
id: docker-digest
run: |
mkdir -p ~/.cache/docker/${{ env.PLEX_CONTAINER }}
echo "Image: ${{ env.PLEX_CONTAINER }}"
echo "Tag: ${{ env.PLEX_CONTAINER_TAG }}"
token=$(curl \
--silent \
"https://auth.docker.io/token?scope=repository:${{ env.PLEX_CONTAINER }}:pull&service=registry.docker.io" \
| jq -r '.token')
digest=$(curl \
--silent \
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
--header "Authorization: Bearer $token" \
"https://registry-1.docker.io/v2/${{ env.PLEX_CONTAINER }}/manifests/${{ env.PLEX_CONTAINER_TAG }}" \
| jq -r '.config.digest')
echo "Digest: $digest"
echo ::set-output name=digest::$digest

- name: Cache PMS Docker image
id: docker-cache
uses: actions/cache@v3
with:
path: ~/.cache/docker/linuxserver/*.tar
key: ${{ runner.os }}-plex-${{ steps.docker-digest.outputs.digest }}

- name: Pull PMS Docker image
if: steps.docker-cache.outputs.cache-hit != 'true'
run: |
docker pull ${{ env.PLEX_CONTAINER }}:${{ env.PLEX_CONTAINER_TAG }}
docker save -o ~/.cache/docker/${{ env.PLEX_CONTAINER }}-${{ env.PLEX_CONTAINER_TAG }}.tar ${{ env.PLEX_CONTAINER }}:${{ env.PLEX_CONTAINER_TAG }}
echo "Saved image: ${{ env.PLEX_CONTAINER }}:${{ env.PLEX_CONTAINER_TAG }}"

- name: Load PMS Docker image
if: steps.docker-cache.outputs.cache-hit == 'true'
run: |
docker load -i ~/.cache/docker/${{ env.PLEX_CONTAINER }}-${{ env.PLEX_CONTAINER_TAG }}.tar

- name: Set Plex credentials
run: |
echo "PLEXAPI_AUTH_SERVER_TOKEN=${{ secrets.PLEXAPI_AUTH_SERVER_TOKEN }}" >> $GITHUB_ENV

- name: Restore fake media files
id: cache-data
uses: actions/cache@v3
with:
path: ~/.cache/data
key: ${{ env.CACHE_VERSION }}-${{ runner.os }}-fake-media

- name: Create fake media
if: steps.cache-data.outputs.cache-hit != 'true'
run: |
sudo apt-get install -y ffmpeg
mkdir ~/.cache/data
echo "Generating subtitles..."
printf "1\n00:00:00,000 --> 00:00:01,000\nSubtitle" > ~/.cache/data/empty.srt
echo "Generating audio..."
ffmpeg -hide_banner -loglevel error -f lavfi -i anullsrc -t 3 -c:a libvorbis ~/.cache/data/empty.wav
echo "Generating video..."
ffmpeg -hide_banner -loglevel error -f lavfi -t 3 -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p ~/.cache/data/empty.mkv
echo "Muxing everything..."
ffmpeg -hide_banner -loglevel error -i ~/.cache/data/empty.mkv -i ~/.cache/data/empty.wav -i ~/.cache/data/empty.srt \
-c copy -map 0:v:0 -map 1:a:0 -map 1:a:0 \
-map 2:s:0 -map 2:s:0 \
-metadata:s:a:0 language=eng \
-metadata:s:a:1 language=fra \
-metadata:s:s:0 language=eng \
-metadata:s:s:1 language=fra \
-metadata:s:s:1 forced=true \
~/.cache/data/all.mkv

- name: Bootstrap ${{ env.DEFAULT_PLEX }} Plex server
run: |
. venv/bin/activate
python \
-u tools/plex_bootstrap.py \
--destination plex \
--advertise-ip 127.0.0.1 \
--bootstrap-timeout 540 \
--docker-tag ${{ env.PLEX_CONTAINER_TAG }} \
--${{ env.DEFAULT_PLEX }}

- name: Main tests with ${{ env.DEFAULT_PLEX }} server
run: |
. venv/bin/activate
pytest \
-rxXs \
--tb=native \
--verbose \
--cov=plex_auto_languages \
--cov-report term-missing \
--cov-append \
tests

- name: Unlink PMS from MyPlex account
if: always()
run: |
. venv/bin/activate
python -u tools/plex_teardown.py

- name: Upload coverage artifact
uses: actions/upload-artifact@v3
with:
name: coverage-${{ env.DEFAULT_PLEX }}-${{ env.DEFAULT_PYTHON }}
path: .coverage

coverage:
name: upload coverage to codecov
runs-on: ubuntu-20.04
needs: pytest
if: always()
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3

- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v3
with:
python-version: ${{ env.DEFAULT_PYTHON }}

- name: Restore Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache@v3
with:
path: venv
key: ${{ env.CACHE_VERSION }}-${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}

- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
echo "Failed to restore Python virtual environment from cache"
exit 1

- name: Download all coverage artifacts
uses: actions/download-artifact@v3

- name: Combine ${{ env.DEFAULT_PLEX }} coverage results
run: |
. venv/bin/activate
coverage combine coverage-${{ env.DEFAULT_PLEX }}*/.coverage*
coverage report --fail-under=50
coverage xml

- name: Upload ${{ env.DEFAULT_PLEX }} coverage to Codecov
uses: codecov/[email protected]
with:
flags: ${{ env.DEFAULT_PLEX }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.pyc
.vscode/
*.tar
*.tar
.pytest_cache
.coverage
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![GitHub Build](https://img.shields.io/github/workflow/status/RemiRigal/Plex-Auto-Languages/dockerhub_build_push?style=flat-square)](https://github.com/RemiRigal/Plex-Auto-Languages/actions/workflows/dockerhub_build_push.yml)
[![Docker Pulls](https://img.shields.io/docker/pulls/remirigal/plex-auto-languages?style=flat-square)](https://hub.docker.com/r/remirigal/plex-auto-languages)
[![Version](https://img.shields.io/github/v/tag/RemiRigal/Plex-Auto-Languages?style=flat-square&label=version)](https://github.com/RemiRigal/Plex-Auto-Languages/tags)
[![Codecov](https://img.shields.io/codecov/c/gh/RemiRigal/Plex-Auto-Languages/master?flag=flag_name&token=8cb10651-9838-444a-a9d0-2ae5f65379dc&style=flat-square&logo=codecov)](https://app.codecov.io/gh/RemiRigal/Plex-Auto-Languages)
[![License](https://img.shields.io/github/license/RemiRigal/Plex-Auto-Languages?style=flat-square)](https://github.com/RemiRigal/Plex-Auto-Languages/blob/master/LICENSE)

This application lets you have a Netflix-like experience by auto-updating the language of your Plex TV Show episodes based on the current language you are using without messing with your existing language preferences.
Expand Down
2 changes: 1 addition & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def start(self):
self.plex.save_cache()
if self.scheduler:
logger.info("Stopping scheduler")
self.scheduler.stop_event.set()
self.scheduler.shutdown()
logger.info("Stopping alert listener")
self.healthcheck_server.shutdown()

Expand Down
7 changes: 7 additions & 0 deletions plex_auto_languages/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

class InvalidConfiguration(Exception):
pass


class UserNotFound(Exception):
pass
37 changes: 27 additions & 10 deletions plex_auto_languages/plex_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
import itertools
from typing import Union
from datetime import datetime, timedelta
Expand All @@ -15,6 +14,7 @@
from plex_auto_languages.utils.notifier import Notifier
from plex_auto_languages.plex_server_cache import PlexServerCache
from plex_auto_languages.constants import EventType
from plex_auto_languages.exceptions import UserNotFound


logger = get_logger()
Expand Down Expand Up @@ -78,26 +78,34 @@ def __init__(self, url: str, token: str, notifier: Notifier, config: Configurati
super().__init__(url, token)
self.notifier = notifier
self.config = config
self.user_id, self.username = self._get_user_id()
if self.user_id is None:
self._user = self._get_logged_user()
if self._user is None:
logger.error("Unable to find the user associated with the provided Plex Token")
sys.exit(0)
raise UserNotFound
else:
logger.info(f"Successfully connected as user '{self.username}' (id: {self.user_id})")
self._alert_handler = None
self._alert_listener = None
self.cache = PlexServerCache(self)

@property
def user_id(self):
return self._user.id if self._user is not None else None

@property
def username(self):
return self._user.name if self._user is not None else None

@property
def is_alive(self):
return self._alert_listener is not None and self._alert_listener.is_alive()

def _get_user_id(self):
def _get_logged_user(self):
plex_username = self._plex.myPlexAccount().username
for account in self._plex.systemAccounts():
if account.name == plex_username:
return account.id, account.name
return None, None
return account
return None

def save_cache(self):
self.cache.save()
Expand All @@ -109,13 +117,22 @@ def start_alert_listener(self):
self._alert_handler = PlexAlertHandler(self, trigger_on_play, trigger_on_scan, trigger_on_activity)
self._alert_listener = self._plex.startAlertListener(self._alert_handler)

def get_instance_users(self):
users = []
for user in self._plex.myPlexAccount().users():
server_identifiers = [share.machineIdentifier for share in user.servers]
if self.unique_id in server_identifiers:
user.name = user.title
users.append(user)
return users

def get_all_user_ids(self):
return [self.user_id] + [user.id for user in self._plex.myPlexAccount().users()]
return [self.user_id] + [user.id for user in self.get_instance_users()]

def get_plex_instance_of_user(self, user_id: Union[int, str]):
if str(self.user_id) == str(user_id):
return self
matching_users = [u for u in self._plex.myPlexAccount().users() if str(u.id) == str(user_id)]
matching_users = [u for u in self.get_instance_users() if str(u.id) == str(user_id)]
if len(matching_users) == 0:
logger.error(f"Unable to find user with id '{user_id}'")
return None
Expand All @@ -135,7 +152,7 @@ def get_user_from_client_identifier(self, client_identifier: str):
return (user.id, user.name)

def get_user_by_id(self, user_id: Union[int, str]):
matching_users = [u for u in self._plex.systemAccounts() if str(u.id) == str(user_id)]
matching_users = [u for u in [self._user] + self.get_instance_users() if str(u.id) == str(user_id)]
if len(matching_users) == 0:
return None
return matching_users[0]
Expand Down
Loading