diff --git a/.flake8 b/.flake8 index 20d6426..bb0de89 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,16 @@ [flake8] max-line-length = 120 -ignore = - S101 +per-file-ignores = tests/*.py: E501, S101 +exclude = + .git + __pycache__ + setup.py + build + dist + releases + .venv + .tox + .mypy_cache + .pytest_cache + .vscode + .github \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3411b43..4a9ffac 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -6,8 +6,8 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] - poetry-version: ["1.1.14"] + python-version: ["3.8", "3.9", "3.10", "3.11"] + poetry-version: ["1.3.1"] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/README.md b/README.md index b7d1640..a73a047 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,5 @@ kisskh -vv dl "https://kisskh.me/Drama/A-Business-Proposal?id=4608" -f 3 -l 3 -q --- # :construction: TODO -- [ ] Add unit test - [ ] Add ability to export all download link - [ ] Add ability to open stream in some player diff --git a/poetry.lock b/poetry.lock index 2918d29..b750aba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1855,6 +1855,33 @@ files = [ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] +[[package]] +name = "types-requests" +version = "2.28.11.15" +description = "Typing stubs for requests" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-requests-2.28.11.15.tar.gz", hash = "sha256:fc8eaa09cc014699c6b63c60c2e3add0c8b09a410c818b5ac6e65f92a26dde09"}, + {file = "types_requests-2.28.11.15-py3-none-any.whl", hash = "sha256:a05e4c7bc967518fba5789c341ea8b0c942776ee474c7873129a61161978e586"}, +] + +[package.dependencies] +types-urllib3 = "<1.27" + +[[package]] +name = "types-urllib3" +version = "1.26.25.8" +description = "Typing stubs for urllib3" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-urllib3-1.26.25.8.tar.gz", hash = "sha256:ecf43c42d8ee439d732a1110b4901e9017a79a38daca26f08e42c8460069392c"}, + {file = "types_urllib3-1.26.25.8-py3-none-any.whl", hash = "sha256:95ea847fbf0bf675f50c8ae19a665baedcf07e6b4641662c4c3c72e7b2edf1a9"}, +] + [[package]] name = "typing-extensions" version = "4.5.0" @@ -2019,4 +2046,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "a52204ba5f11e66e0272c345e6f95e4097b545f7465f00006891810496fb387d" +content-hash = "155d9bac7ecf2c869ef3de7db08ec7943c9a135ffcd6544277251bf8e1d06070" diff --git a/pyproject.toml b/pyproject.toml index 0308319..b3577cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ pep8-naming = "^0.13.3" pytest-cov = "^4.0.0" mypy = "^1.0.1" black = "^23.1.0" +types-requests = "^2.28.11.15" [tool.black] line-length = 120 diff --git a/src/kisskh_downloader/cli.py b/src/kisskh_downloader/cli.py index 49d6056..dd3e56c 100644 --- a/src/kisskh_downloader/cli.py +++ b/src/kisskh_downloader/cli.py @@ -2,7 +2,7 @@ import re import sys from pathlib import Path -from typing import List, Union +from typing import Dict, List, Union from urllib.parse import parse_qs, urlparse import click @@ -58,7 +58,7 @@ def dl( logger = logging.getLogger(__name__) kisskh_api = KissKHApi() downloader = Downloader(referer="https://kisskh.me") - episode_ids = [] + episode_ids: Dict[int, int] = {} if validators.url(drama_url_or_name): parsed_url = urlparse(drama_url_or_name) ids = parse_qs(parsed_url.query).get("id") @@ -68,10 +68,10 @@ def dl( episode_id = parse_qs(parsed_url.query).get("ep") episode_number = None if episode_string := re.search(r"Episode-(\d+)", parsed_url.path): - episode_number = int(episode_string.group(1)) + episode_number = episode_string.group(1) if episode_id and episode_number: - episode_ids = {episode_number: episode_id[0]} - drama_name = parsed_url.path.split("/")[-1].replace("-", "_") + episode_ids = {int(episode_number): int(episode_id[0])} + drama_name = parsed_url.path.split("/")[2].replace("-", "_") else: drama = kisskh_api.get_drama_by_query(drama_url_or_name) if drama is None: @@ -83,10 +83,10 @@ def dl( if not episode_ids: episode_ids = kisskh_api.get_episode_ids(drama_id=drama_id, start=first, stop=last) - for episode_number, episode_id in episode_ids.items(): + for episode_number, episode_id in episode_ids.items(): # type: ignore logger.info(f"Getting details for Episode {episode_number}...") - video_stream_url = kisskh_api.get_stream_url(episode_id) - subtitles = kisskh_api.get_subtitles(episode_id, *sub_langs) + video_stream_url = kisskh_api.get_stream_url(episode_id) # type: ignore + subtitles = kisskh_api.get_subtitles(episode_id, *sub_langs) # type: ignore if "tickcounter" in video_stream_url: logger.warning(f"Episode {episode_number} still not released!") continue diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/test_drama.py b/tests/models/test_drama.py new file mode 100644 index 0000000..b622a77 --- /dev/null +++ b/tests/models/test_drama.py @@ -0,0 +1,44 @@ +import pytest + +from kisskh_downloader.models.drama import Drama + + +@pytest.fixture +def obj() -> Drama: + reponse_data = { + "description": "Nam Haeng Sun used to be a national athlete. She now runs a side dish store. She has a super positive personality and unlimited like stamina. She takes another turn and enters the private education field, which is for students preparing for their university entrance exam. Unexpectedly, Nam Haeng Sun gets involved with Choi Chi Yeol.\r\n\r\nChoi Chi Yeol is a popular instructor in the private education field and is known as Ilta Instructor (most popular instructor). He works hard at his job. As an instructor to his students, he speaks without reserve and implements showmanship in his lessons. He has accumulated wealth and fame as a popular instructor, but, with increasing success, he has become more sensitive, prickly, and indifferent to people. He then meets Nam Haeng Sun with her super positive personality and never ending stamina. The relationship between Nam Haeng Sun and Choi Chi Yeol develops romantically.", + "releaseDate": "2023-01-14T11:44:28", + "trailer": "", + "country": "South Korea", + "status": "Ongoing", + "type": "TVSeries", + "nextEpDateID": 0, + "episodes": [ + {"id": 120047, "number": 14.0, "sub": 3}, + {"id": 119964, "number": 13.0, "sub": 3}, + {"id": 119566, "number": 12.0, "sub": 3}, + {"id": 119505, "number": 11.0, "sub": 3}, + {"id": 118960, "number": 10.0, "sub": 3}, + {"id": 118906, "number": 9.0, "sub": 3}, + {"id": 118254, "number": 8.0, "sub": 3}, + {"id": 118201, "number": 7.0, "sub": 3}, + {"id": 117787, "number": 6.0, "sub": 3}, + {"id": 117706, "number": 5.0, "sub": 3}, + {"id": 117415, "number": 4.0, "sub": 3}, + {"id": 117370, "number": 3.0, "sub": 3}, + {"id": 116935, "number": 2.0, "sub": 3}, + {"id": 116882, "number": 1.0, "sub": 3}, + ], + "episodesCount": 14, + "label": None, + "favoriteID": 0, + "thumbnail": "https://occ-0-64-58.1.nflxso.net/dnm/api/v6/6gmvu2hxdfnQ55LZZjyzYR4kzGk/AAAABTYthAqSsuZtddo5amrRXv3iKR-LXDnpIs_YOHAt9-QP5CPZqwZy7NC4Nt15TY3dRtgmXE03Dgn4ViuPQK5RVFouQ0krYoVYlwGXYYdX5odL29aWy_n9Y_IPF7NzYxOzHyAW0g.jpg?r=2da", + "id": 6917, + "title": "Crash Course in Romance", + } + return Drama.parse_obj(reponse_data) + + +def test_get_episodes_ids(obj: Drama) -> None: + assert obj.get_episodes_ids(1, 2) == {1: 116882, 2: 116935} + assert obj.get_episodes_ids(6, 11) == {6: 117787, 7: 118201, 8: 118254, 9: 118906, 10: 118960, 11: 119505} diff --git a/tests/test_kisskh_api.py b/tests/test_kisskh_api.py new file mode 100644 index 0000000..4776a2f --- /dev/null +++ b/tests/test_kisskh_api.py @@ -0,0 +1,214 @@ +from unittest.mock import MagicMock + +import pytest + +from kisskh_downloader.kisskh_api import KissKHApi +from kisskh_downloader.models.search import DramaInfo, Search +from kisskh_downloader.models.sub import SubItem + + +@pytest.fixture(scope="module") +def kisskh_api(): + return KissKHApi() + + +def test_get_episode_ids(kisskh_api): + mock_response = MagicMock() + mock_response.json.return_value = { + "description": "Nam Haeng Sun used to be a national athlete. She now runs a side dish store. She has a super positive personality and unlimited like stamina. She takes another turn and enters the private education field, which is for students preparing for their university entrance exam. Unexpectedly, Nam Haeng Sun gets involved with Choi Chi Yeol.\r\n\r\nChoi Chi Yeol is a popular instructor in the private education field and is known as Ilta Instructor (most popular instructor). He works hard at his job. As an instructor to his students, he speaks without reserve and implements showmanship in his lessons. He has accumulated wealth and fame as a popular instructor, but, with increasing success, he has become more sensitive, prickly, and indifferent to people. He then meets Nam Haeng Sun with her super positive personality and never ending stamina. The relationship between Nam Haeng Sun and Choi Chi Yeol develops romantically.", + "releaseDate": "2023-01-14T11:44:28", + "trailer": "", + "country": "South Korea", + "status": "Ongoing", + "type": "TVSeries", + "nextEpDateID": 0, + "episodes": [ + {"id": 120047, "number": 14.0, "sub": 3}, + {"id": 119964, "number": 13.0, "sub": 3}, + {"id": 119566, "number": 12.0, "sub": 3}, + {"id": 119505, "number": 11.0, "sub": 3}, + {"id": 118960, "number": 10.0, "sub": 3}, + {"id": 118906, "number": 9.0, "sub": 3}, + {"id": 118254, "number": 8.0, "sub": 3}, + {"id": 118201, "number": 7.0, "sub": 3}, + {"id": 117787, "number": 6.0, "sub": 3}, + {"id": 117706, "number": 5.0, "sub": 3}, + {"id": 117415, "number": 4.0, "sub": 3}, + {"id": 117370, "number": 3.0, "sub": 3}, + {"id": 116935, "number": 2.0, "sub": 3}, + {"id": 116882, "number": 1.0, "sub": 3}, + ], + "episodesCount": 14, + "label": None, + "favoriteID": 0, + "thumbnail": "https://occ-0-64-58.1.nflxso.net/dnm/api/v6/6gmvu2hxdfnQ55LZZjyzYR4kzGk/AAAABTYthAqSsuZtddo5amrRXv3iKR-LXDnpIs_YOHAt9-QP5CPZqwZy7NC4Nt15TY3dRtgmXE03Dgn4ViuPQK5RVFouQ0krYoVYlwGXYYdX5odL29aWy_n9Y_IPF7NzYxOzHyAW0g.jpg?r=2da", + "id": 6917, + "title": "Crash Course in Romance", + } + kisskh_api._request = MagicMock(return_value=mock_response) + + assert kisskh_api.get_episode_ids(44377, 1, 10) == { + 1: 116882, + 2: 116935, + 3: 117370, + 4: 117415, + 5: 117706, + 6: 117787, + 7: 118201, + 8: 118254, + 9: 118906, + 10: 118960, + } + + kisskh_api._request.assert_called_once_with("https://kisskh.me/api/DramaList/Drama/44377") + + assert kisskh_api.get_episode_ids(44377, 10, 100) == {10: 118960, 11: 119505, 12: 119566, 13: 119964, 14: 120047} + + +def test_get_subtitles(kisskh_api): + mock_response = MagicMock() + mock_response.json.return_value = [ + { + "src": "https://i.filecache.club/mau/d-p-/English-EP12-KinnPorsche-The-Series.srt", + "label": "English", + "land": "en", + "default": False, + }, + {"src": "https://i.filecache.club/mul/km/rsrwxdno.srt", "label": "Khmer", "land": "km", "default": False}, + { + "src": "https://i.filecache.club/mau/d-p-/Bahasa-Indonesia-EP12-KinnPorsche-The-Series.srt", + "label": "Indonesia", + "land": "id", + "default": False, + }, + { + "src": "https://i.filecache.club/mau/d-p-/Bahasa-Malaysia-EP12-KinnPorsche-The-Series.srt", + "label": "Malay", + "land": "ms", + "default": False, + }, + { + "src": "https://i.filecache.club/mau/d-p-/Arabic-EP12-KinnPorsche-The-Series.srt", + "label": "Arabic", + "land": "ar", + "default": False, + }, + ] + kisskh_api._request = MagicMock(return_value=mock_response) + + assert kisskh_api.get_subtitles(18609, "en", "km", "ar") == [ + SubItem( + src="https://i.filecache.club/mau/d-p-/English-EP12-KinnPorsche-The-Series.srt", + label="English", + land="en", + default=False, + ), + SubItem(src="https://i.filecache.club/mul/km/rsrwxdno.srt", label="Khmer", land="km", default=False), + SubItem( + src="https://i.filecache.club/mau/d-p-/Arabic-EP12-KinnPorsche-The-Series.srt", + label="Arabic", + land="ar", + default=False, + ), + ] + + kisskh_api._request.assert_called_once_with("https://kisskh.me/api/Sub/18609") + + assert kisskh_api.get_subtitles(18609, "all") == [ + SubItem( + src="https://i.filecache.club/mau/d-p-/English-EP12-KinnPorsche-The-Series.srt", + label="English", + land="en", + default=False, + ), + SubItem(src="https://i.filecache.club/mul/km/rsrwxdno.srt", label="Khmer", land="km", default=False), + SubItem( + src="https://i.filecache.club/mau/d-p-/Bahasa-Indonesia-EP12-KinnPorsche-The-Series.srt", + label="Indonesia", + land="id", + default=False, + ), + SubItem( + src="https://i.filecache.club/mau/d-p-/Bahasa-Malaysia-EP12-KinnPorsche-The-Series.srt", + label="Malay", + land="ms", + default=False, + ), + SubItem( + src="https://i.filecache.club/mau/d-p-/Arabic-EP12-KinnPorsche-The-Series.srt", + label="Arabic", + land="ar", + default=False, + ), + ] + + +def test_search_dramas_by_query(kisskh_api): + mock_response = MagicMock() + mock_response.json.return_value = [ + { + "episodesCount": 16, + "label": "", + "favoriteID": 0, + "thumbnail": "https://occ-0-2306-64.1.nflxso.net/dnm/api/v6/6AYY37jfdO6hpXcMjf9Yu5cnmO0/AAAABYls93eGni5U0Uvfx2k9k23JLjbf25st6tq2ksurtegY41ReNjxtC37xtlsOTIgakHN1wSIbRhc7TVZJWPid653PmvnE.jpg?r=cbd", + "id": 97, + "title": "Crash Landing on You", + }, + { + "episodesCount": 14, + "label": "", + "favoriteID": 0, + "thumbnail": "https://occ-0-64-58.1.nflxso.net/dnm/api/v6/6gmvu2hxdfnQ55LZZjyzYR4kzGk/AAAABTYthAqSsuZtddo5amrRXv3iKR-LXDnpIs_YOHAt9-QP5CPZqwZy7NC4Nt15TY3dRtgmXE03Dgn4ViuPQK5RVFouQ0krYoVYlwGXYYdX5odL29aWy_n9Y_IPF7NzYxOzHyAW0g.jpg?r=2da", + "id": 6917, + "title": "Crash Course in Romance", + }, + ] + kisskh_api._request = MagicMock(return_value=mock_response) + + search_result = kisskh_api.search_dramas_by_query("Crash") + + kisskh_api._request.assert_called_once_with("https://kisskh.me/api/DramaList/Search?q=Crash") + + assert search_result == Search( + __root__=[ + DramaInfo( + episodesCount=16, + label="", + favoriteID=0, + thumbnail="https://occ-0-2306-64.1.nflxso.net/dnm/api/v6/6AYY37jfdO6hpXcMjf9Yu5cnmO0/AAAABYls93eGni5U0Uvfx2k9k23JLjbf25st6tq2ksurtegY41ReNjxtC37xtlsOTIgakHN1wSIbRhc7TVZJWPid653PmvnE.jpg?r=cbd", + id=97, + title="Crash Landing on You", + ), + DramaInfo( + episodesCount=14, + label="", + favoriteID=0, + thumbnail="https://occ-0-64-58.1.nflxso.net/dnm/api/v6/6gmvu2hxdfnQ55LZZjyzYR4kzGk/AAAABTYthAqSsuZtddo5amrRXv3iKR-LXDnpIs_YOHAt9-QP5CPZqwZy7NC4Nt15TY3dRtgmXE03Dgn4ViuPQK5RVFouQ0krYoVYlwGXYYdX5odL29aWy_n9Y_IPF7NzYxOzHyAW0g.jpg?r=2da", + id=6917, + title="Crash Course in Romance", + ), + ] + ) + + +def test_get_stream_url(kisskh_api): + mock_response = MagicMock() + mock_response.json.return_value = { + "Video": "https://hls05.hls1.online/hls05/e4b349db73114f317702a1bb1a8d7f93/ep.12.v0.1657615499.720.m3u8", + "Video_tmp": "", + "ThirdParty": "https://ssbstream.net/e/0334lvognaqc?caption_1=https://sub.dembed1.com&sub_1=English", + "Type": 1, + "id": None, + "dataSaver": None, + "a": None, + "b": None, + "dType": None, + } + kisskh_api._request = MagicMock(return_value=mock_response) + + assert ( + kisskh_api.get_stream_url(13915) + == "https://hls05.hls1.online/hls05/e4b349db73114f317702a1bb1a8d7f93/ep.12.v0.1657615499.720.m3u8" + ) + + kisskh_api._request.assert_called_once_with("https://kisskh.me/api/DramaList/Episode/13915.png?err=false&ts=&time=") diff --git a/tests/test_kisskh_downloader.py b/tests/test_kisskh_downloader.py deleted file mode 100644 index e9fefe3..0000000 --- a/tests/test_kisskh_downloader.py +++ /dev/null @@ -1,5 +0,0 @@ -from kisskh_downloader import __version__ - - -def test_version(): - assert __version__ == "0.1.0"