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()