diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index e0a6693..6c8bbd6 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -7,18 +7,18 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10"]
+ python-version: ['3.8', '3.9', '3.10']
steps:
- - 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 pylint
- pip install -r requirements.txt
- - name: Analysing the code with pylint
- run: |
- pylint $(git ls-files '*.py')
+ - uses: actions/checkout@v4.1.1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pylint
+ pip install -r requirements.txt
+ - name: Analysing the code with pylint
+ run: |
+ pylint $(git ls-files '*.py')
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 9834753..51cc3f1 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -7,26 +7,26 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10"]
+ python-version: ['3.8', '3.9', '3.10']
steps:
- - uses: actions/checkout@v4.1.1
- - 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 -r requirements.txt
- pip install coverage
- - name: Run tests
- run: |
- coverage run -m unittest discover
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v4.0.1
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- - name: Upload coverage to Code Climate
- uses: paambaati/codeclimate-action@v5.0.0
- env:
- CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE_TOKEN }}
+ - uses: actions/checkout@v4.1.1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install coverage
+ - name: Run tests
+ run: |
+ coverage run -m unittest discover
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4.0.1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ - name: Upload coverage to Code Climate
+ uses: paambaati/codeclimate-action@v5.0.0
+ env:
+ CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE_TOKEN }}
diff --git a/plexorcist.py b/plexorcist.py
index f3682ce..c3fd251 100755
--- a/plexorcist.py
+++ b/plexorcist.py
@@ -188,9 +188,11 @@ def convert_to_library_ids(self, libraries):
available_libraries = self.get_available_libraries()
return [
- int(library)
- if library.isdigit()
- else self.get_library_id_by_name(library, available_libraries)
+ (
+ int(library)
+ if library.isdigit()
+ else self.get_library_id_by_name(library, available_libraries)
+ )
for library in libraries
if library
]
@@ -204,8 +206,8 @@ def get_available_libraries(self):
)
if response is not None:
- data = xmltodict.parse(response.content)
- return data["MediaContainer"]["Directory"]
+ data = xmltodict.parse(response.content, force_list=True)
+ return data["MediaContainer"][0]["Directory"]
return []
@@ -220,16 +222,16 @@ def get_library_id_by_name(self, library_name, available_libraries):
def handle_videos(self, response):
"""Handle videos"""
- data = xmltodict.parse(response.content)
- videos = data["MediaContainer"]["Video"]
- media_type = data["MediaContainer"]["@viewGroup"]
+ data = xmltodict.parse(response.content, force_list=True)
+ videos = data["MediaContainer"][0]["Video"]
+ media_type = data["MediaContainer"][0]["@viewGroup"]
if videos and len(videos) > 0:
# Filter watched videos
- watched_videos = self.filter_videos(videos=videos)
+ watched_videos = self.filter_videos(videos)
# Delete watched videos and send notification
- self.delete_videos(watched_videos=watched_videos, media_type=media_type)
+ self.delete_videos(watched_videos, media_type)
def filter_videos(self, videos):
"""Filter videos"""
@@ -237,7 +239,8 @@ def filter_videos(self, videos):
# Check if video was watched and / or is older than
def is_watched_video(video):
return (
- video.get("@viewCount")
+ isinstance(video, dict)
+ and video.get("@viewCount")
and int(video["@viewCount"]) >= 1
and (
self.config["older_than"] == 0
@@ -252,43 +255,49 @@ def is_watched_video(video):
return watched_videos
- def delete_videos(self, watched_videos, media_type):
- """Delete watched videos and send notification"""
+ def get_title(self, video, media_type):
+ """Get the video title"""
- # Get the video title
- def get_title(video):
- if media_type == "show":
- series = video.get("@grandparentTitle", "")
- return f"{series} - {video['@title']}"
+ if media_type == "show":
+ series = video.get("@grandparentTitle", "")
+ return f"{series} - {video['@title']}"
- return video["@title"]
+ return video["@title"]
- # Check if the video is whitelisted
- def is_whitelisted(video):
- title = get_title(video)
- check = (
- title in self.config["whitelist"]
- or video.get("@grandparentTitle", "") in self.config["whitelist"]
- )
- if check:
- logging.info(self.config["i18n"]["whitelisted"].format(title))
- return check
+ def is_whitelisted(self, video, media_type):
+ """Check if the video is whitelisted"""
- # Get the video size
- def get_size(video):
- return round(int(video["Media"]["Part"]["@size"]) / (1024 * 1024), 2)
+ title = self.get_title(video, media_type)
+ check = (
+ title in self.config["whitelist"]
+ or video.get("@grandparentTitle", "") in self.config["whitelist"]
+ )
+ if check:
+ logging.info(self.config["i18n"]["whitelisted"].format(title))
+ return check
- # Delete the video
- def delete_video(video):
- self.util.make_request(
- url=self.config["plex_base"] + video["@key"],
- headers={"X-Plex-Token": self.config["plex_token"]},
- request_type="delete",
- )
- return get_size(video), get_title(video)
+ def get_size(self, video):
+ """Get the video size"""
+
+ return round(int(video["Media"][0]["Part"][0]["@size"]) / (1024 * 1024), 2)
+
+ def delete_video(self, video, media_type):
+ """Delete the video"""
+
+ self.util.make_request(
+ url=self.config["plex_base"] + video["@key"],
+ headers={"X-Plex-Token": self.config["plex_token"]},
+ request_type="delete",
+ )
+ return self.get_size(video), self.get_title(video, media_type)
+
+ def delete_videos(self, watched_videos, media_type):
+ """Delete watched videos and send notification"""
deleted_videos = [
- delete_video(video) for video in watched_videos if not is_whitelisted(video)
+ self.delete_video(video, media_type)
+ for video in watched_videos
+ if not self.is_whitelisted(video, media_type)
]
if deleted_videos:
diff --git a/test_plexorcist.py b/test_plexorcist.py
index 6b4eabd..2ca4469 100644
--- a/test_plexorcist.py
+++ b/test_plexorcist.py
@@ -1,13 +1,307 @@
-"""Main Plexorcist testing file!"""
+#!/usr/bin/env python
+"""Test the main Plexorcist execution file!"""
import unittest
+from unittest.mock import MagicMock, patch
from plexorcist import Plexorcist
class TestPlexorcist(unittest.TestCase):
- """The main Plexorcist testing class"""
+ """The main test class for unit tests
- def setUp(self):
- """Initialize Plexorcist"""
+ Args:
+ unittest (module): Single test cases
+ """
- self.plexorcist = Plexorcist()
+ def test_set_older_than(self):
+ """Test for the _set_older_than method"""
+
+ # Test set_older_than method
+ plexorcist = Plexorcist()
+ plexorcist.config_file = MagicMock()
+ plexorcist.config_file.get.return_value = "1d"
+ # pylint: disable=protected-access
+ older_than = plexorcist._set_older_than()
+
+ # Assertions
+ self.assertGreater(older_than, 0)
+
+ @patch("plexorcist.utils.Utils.make_request")
+ def test_convert_to_library_ids(self, mock_make_request):
+ """Test for the conver_to_library_ids
+
+ Args:
+ mock_make_request (method): Mock the make_request method
+ """
+
+ # Prepare test data
+ mock_response = MagicMock()
+ mock_response.content = (
+ b''
+ b""
+ b''
+ b''
+ b""
+ )
+ mock_make_request.return_value = mock_response
+
+ # Test convert_to_library_ids method
+ plexorcist = Plexorcist()
+ plexorcist.config = {"plex_base": "http://example.com", "plex_token": "token"}
+ library_ids = plexorcist.convert_to_library_ids(["Cinema", "Series"])
+
+ # Assertions
+ self.assertEqual(library_ids, [1, 2])
+
+ @patch("plexorcist.utils.Utils.make_request")
+ def test_get_available_libraries(self, mock_make_request):
+ """Test for the get_available_libraries method
+
+ Args:
+ mock_make_request (method): Mock the make_request method
+ """
+
+ # Prepare test data
+ mock_response = MagicMock()
+ mock_response.content = (
+ b''
+ b""
+ b''
+ b''
+ b""
+ )
+ mock_make_request.return_value = mock_response
+
+ # Test get_available_libraries method
+ plexorcist = Plexorcist()
+ libraries = plexorcist.get_available_libraries()
+
+ # Assertions
+ self.assertEqual(len(libraries), 2)
+ self.assertEqual(libraries[0]["@title"], "Cinema")
+
+ def test_get_library_id_by_name(self):
+ """Test for the get_library_id_by_name method"""
+
+ # Prepare test data
+ plexorcist = Plexorcist()
+ available_libraries = [
+ {"@title": "Cinema", "@key": "1"},
+ {"@title": "Series", "@key": "2"},
+ ]
+ library_id = plexorcist.get_library_id_by_name("Cinema", available_libraries)
+
+ # Assertions
+ self.assertEqual(library_id, 1)
+
+ def test_get_library_id_by_name_found(self):
+ """Test for the get_library_id_by_name when name is found"""
+
+ # Prepare test data
+ available_libraries = [
+ {"@title": "Cinema", "@key": "1"},
+ {"@title": "Series", "@key": "2"},
+ ]
+
+ # Test get_library_id_by_name method when library is found
+ plexorcist = Plexorcist()
+ library_id = plexorcist.get_library_id_by_name("Cinema", available_libraries)
+
+ # Assertions
+ self.assertEqual(library_id, 1)
+
+ def test_get_library_id_by_name_not_found(self):
+ """Test for the get_library_id_by_name when name is not found"""
+
+ # Prepare test data
+ available_libraries = [
+ {"@title": "Cinema", "@key": "1"},
+ {"@title": "Series", "@key": "2"},
+ ]
+
+ # Test get_library_id_by_name method when library is not found
+ plexorcist = Plexorcist()
+ library_id = plexorcist.get_library_id_by_name("Music", available_libraries)
+
+ # Assertions
+ self.assertIsNone(library_id)
+
+ @patch("plexorcist.utils.Utils.make_request")
+ def test_handle_videos(self, mock_make_request):
+ """Test for the handle_videos method
+
+ Args:
+ mock_make_request (method): Mock the make_request method
+ """
+
+ # Prepare test data
+ mock_response = MagicMock()
+ mock_response.content = (
+ b''
+ b''
+ b'"
+ b""
+ )
+ mock_make_request.return_value = mock_response
+
+ # Test handle_videos method
+ plexorcist = Plexorcist()
+ plexorcist.pushbullet = MagicMock()
+ plexorcist.handle_videos(mock_response)
+
+ # Assertions
+ mock_make_request.assert_called_once()
+
+ def test_filter_videos(self):
+ """Test for the filter_videos method"""
+
+ # Prepare test data
+ videos = [
+ {"@viewCount": "1", "@lastViewedAt": "123"},
+ {"@viewCount": "0", "@lastViewedAt": "0"},
+ ]
+
+ # Test filter_videos method
+ plexorcist = Plexorcist()
+ watched_videos = plexorcist.filter_videos(videos)
+
+ # Assertions
+ self.assertEqual(len(watched_videos), 1)
+
+ def test_get_title_show(self):
+ """Test for the get_title method for media type show"""
+
+ # Prepare test data
+ video = {"@grandparentTitle": "Grandparent", "@title": "Title"}
+
+ # Test get_title method for show media type
+ plexorcist = Plexorcist()
+ title = plexorcist.get_title(video, "show")
+
+ # Assertions
+ self.assertEqual(title, "Grandparent - Title")
+
+ def test_get_title_movie(self):
+ """Test for get_title method for media type movie"""
+
+ # Prepare test data
+ video = {"@title": "Title", "@grandparentTitle": "Series 1"}
+
+ # Test get_title method for movie media type
+ plexorcist = Plexorcist()
+ title = plexorcist.get_title(video, "show")
+
+ # Assertions
+ self.assertEqual(title, "Series 1 - Title")
+
+ def test_is_whitelisted(self):
+ """Test for the is_whitelisted method"""
+
+ # Prepare test data
+ video = {"@title": "Title"}
+
+ # Test is_whitelisted method
+ plexorcist = Plexorcist()
+ plexorcist.config = {"whitelist": ["Whitelisted Title"]}
+ is_whitelisted = plexorcist.is_whitelisted(video, "show")
+
+ # Assertions
+ self.assertFalse(is_whitelisted)
+
+ def test_get_size(self):
+ """Test for the get_size method"""
+
+ # Prepare test data
+ video = {"Media": [{"Part": [{"@size": "1024"}]}]}
+
+ # Test get_size method
+ plexorcist = Plexorcist()
+ size = plexorcist.get_size(video)
+
+ # Assertions
+ self.assertEqual(size, 0.0)
+
+ @patch("plexorcist.utils.Utils.make_request")
+ def test_delete_videos(self, mock_make_request):
+ """Test for the delete_videos method
+
+ Args:
+ mock_make_request (method): Mock the make_request method
+ """
+
+ # Prepare test data
+ mock_response = MagicMock()
+ mock_response.content = (
+ b''
+ b''
+ b'"
+ b""
+ )
+ mock_make_request.return_value = mock_response
+ watched_videos = [
+ {
+ "@title": "Title",
+ "@grandparentTitle": "Grandparent",
+ "@key": "/path/to/video",
+ "Media": [{"Part": [{"@size": "1024"}]}],
+ }
+ ]
+
+ # Test delete_videos method
+ plexorcist = Plexorcist()
+ plexorcist.config = {
+ "plex_base": "http://example.com",
+ "plex_token": "token",
+ "ifttt_webhook": "",
+ "i18n": {
+ "removed": "Removed {0} videos, reclaimed {1} GB",
+ "notification": "Notification sent",
+ "ifttt_error": "IFTTT webhook error",
+ },
+ "whitelist": ["Whitelisted Title"],
+ }
+ plexorcist.pushbullet = MagicMock()
+ plexorcist.delete_videos(watched_videos, "movie")
+
+ # Assertions
+ mock_make_request.assert_called_once()
+
+ @patch("plexorcist.utils.Utils.make_request")
+ def test_send_notification(self, mock_make_request):
+ """Test for the send_notification method
+
+ Args:
+ mock_make_request (method): Mock the make_request method
+ """
+ # Prepare test data
+ mock_response = MagicMock()
+ mock_response.content = b""
+ mock_make_request.return_value = mock_response
+
+ # Test send_notification method
+ plexorcist = Plexorcist()
+ plexorcist.config = {
+ "ifttt_webhook": "https://ifttt.com/webhook",
+ "i18n": {
+ "removed": "Removed {0} videos, reclaimed {1} GB",
+ "notification": "Notification sent",
+ "ifttt_error": "IFTTT webhook error",
+ },
+ }
+ plexorcist.pushbullet = MagicMock()
+ plexorcist.send_notification(["Video 1", "Video 2"], 0.5)
+
+ # Assertions
+ mock_make_request.assert_called_once()
+
+
+if __name__ == "__main__":
+ unittest.main()