From 4766a1017dfb8f6cb2f86d3172dbf7753147e746 Mon Sep 17 00:00:00 2001 From: Lovi <62809003+Lovi-0@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:33:10 +0200 Subject: [PATCH] Update openssl check. --- Src/Lib/M3U8/decryptor.py | 9 +- Test/t_domain_google.py | 84 --------------- Test/t_episode_type.py | 96 ----------------- Test/t_search_type.py | 31 ------ Test/t_series_type.py | 33 ------ Test/t_vixcloud.py | 220 -------------------------------------- Test/t_window_type.py | 41 ------- 7 files changed, 6 insertions(+), 508 deletions(-) delete mode 100644 Test/t_domain_google.py delete mode 100644 Test/t_episode_type.py delete mode 100644 Test/t_search_type.py delete mode 100644 Test/t_series_type.py delete mode 100644 Test/t_vixcloud.py delete mode 100644 Test/t_window_type.py diff --git a/Src/Lib/M3U8/decryptor.py b/Src/Lib/M3U8/decryptor.py index 48aef02..4aae97d 100644 --- a/Src/Lib/M3U8/decryptor.py +++ b/Src/Lib/M3U8/decryptor.py @@ -71,11 +71,14 @@ if crypto_installed: else: # Check if openssl command is available - openssl_available = subprocess.run(["openssl", "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 - logging.info("Decrypy use: OPENSSL") + try: + openssl_available = subprocess.run(["openssl", "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + logging.info("Decrypy use: OPENSSL") + except: + openssl_available = False if not openssl_available: - console.log("[red]Neither Crypto nor openssl is installed. Please install either one of them.") + console.log("[red]Neither python library: pycryptodome nor openssl software is installed. Please install either one of them. Read readme.md [Requirement].") sys.exit(0) class M3U8_Decryption: diff --git a/Test/t_domain_google.py b/Test/t_domain_google.py deleted file mode 100644 index 55a9906..0000000 --- a/Test/t_domain_google.py +++ /dev/null @@ -1,84 +0,0 @@ -# 15.05.24 - -# Fix import -import sys -import os -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Import -from Src.Api.Streamingcommunity.Core.Util.get_domain import grab_sc_top_level_domain -from Src.Api.Animeunity.Core.Util.get_domain import grab_au_top_level_domain -import unittest -import time -import logging - - -class URLFilter(logging.Filter): - def __init__(self, url): - super().__init__() - self.url = url - - def filter(self, record): - return self.url not in record.getMessage() - - -# Configure logging -logging.basicConfig() -logger = logging.getLogger() -logger.setLevel(logging.ERROR) # Set logger level to ERROR - - -# Add custom filter to suppress URLs -url_filters = ['https://streamingcommunity', 'https://www.animeunity'] -for url in url_filters: - logger.addFilter(URLFilter(url)) - - -# Variable -real_stream_domain = "foo" -real_anime_domain = "to" - - - -class TestGrabStreamingDomain(unittest.TestCase): - def test_light_streaming(self): - start = time.time() - result = grab_sc_top_level_domain(method='light') - end = time.time() - print(f"Light streaming: {result}, in: {end - start}") - - # Assert that result is as expected - self.assertEqual(result, real_stream_domain) - - def test_strong_streaming(self): - start = time.time() - result = grab_sc_top_level_domain(method='strong') - end = time.time() - print(f"Strong streaming: {result}, in: {end - start}") - - # Assert that result is as expected - self.assertEqual(result, real_stream_domain) - - def test_light_anime(self): - start = time.time() - result = grab_au_top_level_domain(method='light') - end = time.time() - print(f"Light anime: {result}, in: {end - start}") - - # Assert that result is as expected - self.assertEqual(result, real_anime_domain) - - def test_strong_anime(self): - start = time.time() - result = grab_au_top_level_domain(method='strong') - end = time.time() - print(f"Strong anime: {result}, in: {end - start}") - - # Assert that result is as expected - self.assertEqual(result, real_anime_domain) - -if __name__ == '__main__': - unittest.main() - diff --git a/Test/t_episode_type.py b/Test/t_episode_type.py deleted file mode 100644 index d6ac438..0000000 --- a/Test/t_episode_type.py +++ /dev/null @@ -1,96 +0,0 @@ -# 15.05.24 - -# Fix import -import sys -import os -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Import -from Src.Api.Streamingcommunity.Core.Class.EpisodeType import EpisodeManager -import unittest - - -class TestEpisodeManager(unittest.TestCase): - def test_get_length(self): - # Test data - json_ep_1 = { - "id":59517, - "number":1, - "name":"La Fine", - "plot":"Okey dokey...", - "duration":75, - "scws_id":220399, - "season_id":3883, - "created_by":"None", - "created_at":"2024-04-11T01:31:02.000000Z", - "updated_at":"2024-04-11T01:31:02.000000Z", - "images":[ - { - "id":103989, - "filename":"b7bd00f0-c420-471a-8cc0-1877b3c6182b.webp", - "type":"cover", - "imageable_type":"episode", - "imageable_id":59517, - "created_at":"2024-04-11T01:31:02.000000Z", - "updated_at":"2024-04-11T01:31:02.000000Z", - "original_url_field":"None" - } - ] - } - json_ep_2 = {'id': 59525, 'number': 2, 'name': "L'obiettivo", 'plot': 'So che quassù non è stato facile per voi', 'duration': 66, 'scws_id': 220404, 'season_id': 3883, 'created_by': None, 'created_at': '2024-04-11T01:38:02.000000Z', 'updated_at': '2024-04-11T01:38:03.000000Z', 'images': [{'id': 103997, 'filename': 'fe838c36-50d0-4096-b4e3-68228c7c9e40.webp', 'type': 'cover', 'imageable_type': 'episode', 'imageable_id': 59525, 'created_at': '2024-04-11T01:38:02.000000Z', 'updated_at': '2024-04-11T01:38:02.000000Z', 'original_url_field': None}]} - json_ep_3 = {'id': 59531, 'number': 3, 'name': 'La Testa', 'plot': "La Zona Contaminata ha la sua Regola d'Oro...", 'duration': 57, 'scws_id': 220409, 'season_id': 3883, 'created_by': None, 'created_at': '2024-04-11T01:50:02.000000Z', 'updated_at': '2024-04-11T01:50:03.000000Z', 'images': [{'id': 104003, 'filename': 'e0d105aa-01a9-400d-b257-bcd3cddd164c.webp', 'type': 'cover', 'imageable_type': 'episode', 'imageable_id': 59531, 'created_at': '2024-04-11T01:50:02.000000Z', 'updated_at': '2024-04-11T01:50:02.000000Z', 'original_url_field': None}]} - - - # Initialize the episode manager - eps_manager = EpisodeManager() - - # Add episodes to the episode manager - eps_manager.add_episode(json_ep_1) - eps_manager.add_episode(json_ep_2) - eps_manager.add_episode(json_ep_3) - - # Verify if the number of episodes in the episode manager is correct - self.assertEqual(eps_manager.get_length(), 3) - - def test_add_episode(self): - # Test data - json_ep_1 = { - "id":59517, - "number":1, - "name":"La Fine", - "plot":"Okey dokey...", - "duration":75, - "scws_id":220399, - "season_id":3883, - "created_by":"None", - "created_at":"2024-04-11T01:31:02.000000Z", - "updated_at":"2024-04-11T01:31:02.000000Z", - "images":[ - { - "id":103989, - "filename":"b7bd00f0-c420-471a-8cc0-1877b3c6182b.webp", - "type":"cover", - "imageable_type":"episode", - "imageable_id":59517, - "created_at":"2024-04-11T01:31:02.000000Z", - "updated_at":"2024-04-11T01:31:02.000000Z", - "original_url_field":"None" - } - ] - } - - # Initialize the episode manager - eps_manager = EpisodeManager() - - # Add the episode to the episode manager - eps_manager.add_episode(json_ep_1) - - # Check the ID of the first episode added - first_episode_id = eps_manager.episodes[0].id - self.assertEqual(first_episode_id, json_ep_1['id']) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/Test/t_search_type.py b/Test/t_search_type.py deleted file mode 100644 index bdafa13..0000000 --- a/Test/t_search_type.py +++ /dev/null @@ -1,31 +0,0 @@ -# 15.05.24 - -# Fix import -import sys -import os -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Import -from Src.Api.Streamingcommunity.Core.Class.SearchType import MediaManager - - -# Test data -json_media_1 = {'id': 4006, 'slug': 'person-of-interest', 'name': 'Person of Interest', 'type': 'tv', 'score': '8.6', 'sub_ita': 0, 'last_air_date': None, 'seasons_count': 5, 'images': [{'imageable_id': 4006, 'imageable_type': 'title', 'filename': '967d17c0-2a64-43b9-860c-a14b650cfbd3.webp', 'type': 'poster', 'original_url_field': None}, {'imageable_id': 4006, 'imageable_type': 'title', 'filename': '544889c3-67bf-4c4c-999a-5b9480b42c0f.webp', 'type': 'background', 'original_url_field': None}, {'imageable_id': 4006, 'imageable_type': 'title', 'filename': '3c6cb9ec-4cac-4fcc-ae47-eb2bf9f9a397.webp', 'type': 'cover', 'original_url_field': None}, {'imageable_id': 4006, 'imageable_type': 'title', 'filename': '3ce34b59-ff49-41af-9886-b169043e3f4d.webp', 'type': 'cover_mobile', 'original_url_field': None}, {'imageable_id': 4006, 'imageable_type': 'title', 'filename': '9db4b4d2-2148-44c9-bede-c40faa582f33.webp', 'type': 'logo', 'original_url_field': None}]} -json_media_2 = {'id': 2794, 'slug': 'mucca-e-pollo', 'name': 'Mucca e Pollo', 'type': 'tv', 'score': '7.5', 'sub_ita': 0, 'last_air_date': None, 'seasons_count': 4, 'images': [{'imageable_id': 2794, 'imageable_type': 'title', 'filename': '704cc5d9-6e42-4659-a81b-e41c1c626719.webp', 'type': 'poster', 'original_url_field': None}, {'imageable_id': 2794, 'imageable_type': 'title', 'filename': '4608481d-e97c-44c2-8426-0eb86d435b5f.webp', 'type': 'background', 'original_url_field': None}, {'imageable_id': 2794, 'imageable_type': 'title', 'filename': '57240efa-e295-4905-aee5-3240578e3dfe.webp', 'type': 'cover', 'original_url_field': None}, {'imageable_id': 2794, 'imageable_type': 'title', 'filename': '05827a5f-30a1-4c89-ad14-3eac4b4931c9.webp', 'type': 'cover_mobile', 'original_url_field': None}, {'imageable_id': 2794, 'imageable_type': 'title', 'filename': 'e98ab29b-9ea8-4ce0-8dcb-3d28ebaa571c.webp', 'type': 'logo', 'original_url_field': None}]} -json_media_3 = {'id': 5694, 'slug': 'vatican-girl-la-scomparsa-di-emanuela-orlandi', 'name': 'Vatican Girl: la scomparsa di Emanuela Orlandi', 'type': 'tv', 'score': '8.2', 'sub_ita': 0, 'last_air_date': None, 'seasons_count': 1, 'images': [{'imageable_id': 5694, 'imageable_type': 'title', 'filename': '19e7d5c2-d9f0-4467-b1ca-b9d043de57d5.webp', 'type': 'poster', 'original_url_field': None}, {'imageable_id': 5694, 'imageable_type': 'title', 'filename': '6ae75507-4018-4bec-83e1-85718bf8663d.webp', 'type': 'background', 'original_url_field': None}, {'imageable_id': 5694, 'imageable_type': 'title', 'filename': '05c29b8e-e490-49d2-bc36-68ccd1e9183e.webp', 'type': 'cover', 'original_url_field': None}, {'imageable_id': 5694, 'imageable_type': 'title', 'filename': '331c1787-5a64-49f7-baed-ff47df894f2b.webp', 'type': 'cover_mobile', 'original_url_field': None}, {'imageable_id': 5694, 'imageable_type': 'title', 'filename': '70f487dc-08b9-4c95-9da3-53095e9649e5.webp', 'type': 'logo', 'original_url_field': None}]} -json_media_4 = {'id': 861, 'slug': 'pokemon-detective-pikachu', 'name': 'Pokémon Detective Pikachu', 'type': 'movie', 'score': '7.4', 'sub_ita': 0, 'last_air_date': '2019-05-03', 'seasons_count': 0, 'images': [{'imageable_id': 861, 'imageable_type': 'title', 'filename': '2097306c-6a83-438b-911f-7d600b01dbaf.webp', 'type': 'cover_mobile', 'original_url_field': None}, {'imageable_id': 861, 'imageable_type': 'title', 'filename': 'dc443093-e4cc-465b-8283-acf2d9005127.webp', 'type': 'cover_mobile', 'original_url_field': None}, {'imageable_id': 861, 'imageable_type': 'title', 'filename': '057c99f1-b2fb-455c-8ed1-9f33e19c7914.webp', 'type': 'poster', 'original_url_field': None}, {'imageable_id': 861, 'imageable_type': 'title', 'filename': '6598d047-1a82-4f73-adf5-49729a6f1f45.webp', 'type': 'cover', 'original_url_field': None}, {'imageable_id': 861, 'imageable_type': 'title', 'filename': '70368946-6199-41c2-af77-c5c0b698748d.webp', 'type': 'logo', 'original_url_field': None}, {'imageable_id': 861, 'imageable_type': 'title', 'filename': '55707415-c917-404f-a50a-fba1f0fe2df0.webp', 'type': 'background', 'original_url_field': None}]} - - -# Init class media search manager -search_ep = MediaManager() - -search_ep.add_media(json_media_1) -search_ep.add_media(json_media_2) -search_ep.add_media(json_media_3) - - -print(search_ep) -print(search_ep.media_list[0]) -print(search_ep.media_list[0].images[0]) \ No newline at end of file diff --git a/Test/t_series_type.py b/Test/t_series_type.py deleted file mode 100644 index cee8f8d..0000000 --- a/Test/t_series_type.py +++ /dev/null @@ -1,33 +0,0 @@ -# 15.05.24 - -# Fix import -import sys -import os -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Import -from Src.Api.Streamingcommunity.Core.Class.SeriesType import TitleManager - - -# Test data -title_data_1 = {'id': 1809, 'number': 1, 'name': None, 'plot': None, 'release_date': None, 'title_id': 4006, 'created_at': '2023-05-18T14:58:39.000000Z', 'updated_at': '2023-05-18T14:58:39.000000Z', 'episodes_count': 23} -title_data_2 = {'id': 1810, 'number': 2, 'name': None, 'plot': None, 'release_date': None, 'title_id': 4006, 'created_at': '2023-05-18T14:58:40.000000Z', 'updated_at': '2023-05-18T14:58:40.000000Z', 'episodes_count': 22} -title_data_3 = {'id': 1811, 'number': 3, 'name': None, 'plot': None, 'release_date': None, 'title_id': 4006, 'created_at': '2023-05-18T14:58:40.000000Z', 'updated_at': '2023-05-18T14:58:40.000000Z', 'episodes_count': 23} -title_data_4 = {'id': 1812, 'number': 4, 'name': None, 'plot': None, 'release_date': None, 'title_id': 4006, 'created_at': '2023-05-18T14:58:41.000000Z', 'updated_at': '2023-05-18T14:58:41.000000Z', 'episodes_count': 22} -title_data_5 = {'id': 1813, 'number': 5, 'name': None, 'plot': None, 'release_date': None, 'title_id': 4006, 'created_at': '2023-05-18T14:58:41.000000Z', 'updated_at': '2023-05-18T14:58:41.000000Z', 'episodes_count': 13} - - -# Init class series manager -title_man = TitleManager() - -title_man.add_title(title_data_1) -title_man.add_title(title_data_2) -title_man.add_title(title_data_3) -title_man.add_title(title_data_4) -title_man.add_title(title_data_5) - - -print(title_man) -print(title_man.titles[0]) \ No newline at end of file diff --git a/Test/t_vixcloud.py b/Test/t_vixcloud.py deleted file mode 100644 index 6db46c2..0000000 --- a/Test/t_vixcloud.py +++ /dev/null @@ -1,220 +0,0 @@ -# 15.05.24 - -# Fix import -import sys -import os -import logging -from urllib.parse import urlencode -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - - -# Library import -from Src.Lib.Request import requests -import time -import json -import re -import html - -from datetime import datetime - - -def get_current_expiration_timestamp(): - """Get the current Unix timestamp.""" - return int(time.time()) - -def get_current_year_month(): - """Get the current year and month.""" - now = datetime.now() - return now.year, now.month - -def create_json_file(): - """Create a JSON file if it does not exist.""" - if not os.path.exists(JSON_FILE_PATH): - with open(JSON_FILE_PATH, 'w') as file: - json.dump({}, file, indent=4) - -def save_playlist_info(year, month, playlist_info): - """ - Save playlist information for a specific year and month. - - Args: - year (int): The year. - month (int): The month. - playlist_info (dict): Information about the playlist. - """ - logging.info(f"Saving playlist info for {year}-{month}") - with open(JSON_FILE_PATH, 'r') as file: - data = json.load(file) - - year_month_key = f"{year}-{month}" - data[year_month_key] = playlist_info - - with open(JSON_FILE_PATH, 'w') as file: - json.dump(data, file, indent=4) - - - -# Variable -year, month = get_current_year_month() -JSON_FILE_PATH = 'playlist_info.json' - - -def load_playlist_info(year, month): - """ - Load playlist information for a specific year and month. - - Args: - year (int): The year. - month (int): The month. - - Returns: - dict: Information about the playlist for the specified year and month, or None if not found. - """ - logging.info(f"Loading playlist info for {year}-{month}") - if not os.path.exists(JSON_FILE_PATH): - return None - - with open(JSON_FILE_PATH, 'r') as file: - data = json.load(file) - - year_month_key = f"{year}-{month}" - return data.get(year_month_key) - -def _fetch_webpage_text(url): - """ - Fetch webpage text from a given URL. - - Args: - url (str): The URL of the webpage. - - Returns: - str: The text content of the webpage. - """ - logging.info(f"Fetching webpage text from URL: {url}") - response = requests.get(url, timeout=3) - response.raise_for_status() - return html.unescape(response.text) - -def _extract_playlist_info(webpage_text): - """ - Extract playlist information from webpage text. - - Args: - webpage_text (str): The text content of the webpage. - - Returns: - dict: Information about the playlist extracted from the webpage. - """ - logging.info("Extracting playlist info from webpage text") - info_match = re.search(r'data-page="([\s\S]+})"', webpage_text) - info_data = info_match.group(1) - - print("1. =>", info_data) - info = json.loads(info_data) - logging.debug(f"Playlist info extracted: {info}") - return info - -def _extract_iframe_info(playlist_info): - """ - Extract iframe info from playlist information. - - Args: - playlist_info (dict): Information about the playlist. - - Returns: - str: The text content of the iframe. - """ - embed_url = playlist_info['props']['embedUrl'] - logging.info(f"Fetching iframe info from embed URL: {embed_url}") - iframe_text = _fetch_webpage_text(embed_url) - iframe_info_url_match = re.search(r']+src="([^"]+)', iframe_text) - iframe_info_url = iframe_info_url_match.group(1) - - print("2. =>", iframe_info_url) - return _fetch_webpage_text(iframe_info_url) - -def _extract_playlist_url(iframe_info_text): - """ - Extract playlist URL from iframe info text. - - Args: - iframe_info_text (str): The text content of the iframe. - - Returns: - tuple: A tuple containing playlist information and the playlist URL. - """ - logging.info("Extracting playlist URL from iframe info text") - playlist_match = re.search(r'window\.masterPlaylist[^:]+params:[^{]+({[^<]+?})', iframe_info_text) - playlist_info = json.loads(re.sub(r',[^"]+}', '}', playlist_match.group(1).replace('\'', '"'))) - - playlist_url_match = re.search(r'window\.masterPlaylist[^<]+url:[^<]+\'([^<]+?)\'', iframe_info_text) - playlist_url = playlist_url_match.group(1) - - logging.debug(f"Playlist info: {playlist_info}, Playlist URL: {playlist_url}") - return playlist_info, playlist_url - -def _construct_download_url(playlist_info, playlist_url): - """ - Construct download URL from playlist information and URL. - - Args: - playlist_info (dict): Information about the playlist. - playlist_url (str): The URL of the playlist. - - Returns: - str: The constructed download URL. - """ - logging.info("Constructing download URL") - year, month = get_current_year_month() - save_playlist_info(year, month, playlist_info) - query = urlencode(list(playlist_info.items())) - dl_url = playlist_url + '?' + query - logging.debug(f"Download URL constructed: {dl_url}") - return dl_url - -def get_download_url(video_url): - """ - Get the download URL for a video from its URL. - - Args: - video_url (str): The URL of the video. - - Returns: - str: The download URL for the video. - """ - logging.info(f"Getting download URL for video: {video_url}") - webpage_text = _fetch_webpage_text(video_url) - playlist_info = _extract_playlist_info(webpage_text) - scws_id = playlist_info['props']['title']['scws_id'] - - # Get episode scws_id - if scws_id is None: - logging.info("Getting id from series") - scws_id = playlist_info['props']['episode']['scws_id'] - else: - logging.info("Getting id from film") - - loaded_parameter = load_playlist_info(year, month) - if loaded_parameter is None: - iframe_info_text = _extract_iframe_info(playlist_info) - playlist_info, playlist_url = _extract_playlist_url(iframe_info_text) - return _construct_download_url(playlist_info, playlist_url) - - else: - playlist_url = f"https://vixcloud.co/playlist/{scws_id}?{urlencode(list(loaded_parameter.items()))}" - return playlist_url - -if __name__ == "__main__": - - # Create json file - create_json_file() - - # Get m3u8 playlist from watch url - video_url = "https://streamingcommunity.foo/watch/8427?e=61248" - download_url = get_download_url(video_url) - print(download_url) diff --git a/Test/t_window_type.py b/Test/t_window_type.py deleted file mode 100644 index d799289..0000000 --- a/Test/t_window_type.py +++ /dev/null @@ -1,41 +0,0 @@ -# 15.05.24 - -# Fix import -import sys -import os -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.append(src_path) - - -# Import -from Src.Api.Streamingcommunity.Core.Class.WindowType import WindowVideo, WindowParameter -import unittest - - -class TestWindowVideo(unittest.TestCase): - def test_str(self): - # Test data - json_video = {'id': 220399, 'name': 'Fallout S:1 E:1', 'filename': 'Fallout.S01E01.La.Fine.1080p.AMZN.WEB-DL.ITA.ENG.DDP5.1.H.264.mkv', 'size': 5250906, 'quality': 1080, 'duration': 4498, 'views': 0, 'is_viewable': 1, 'status': 'public', 'fps': 24, 'legacy': 0, 'folder_id': '81aff513-a0c4-4723-bacc-fca2d1143eb5', 'created_at_diff': '3 settimane fa'} - expected_output = "WindowVideo(id=220399, name='Fallout S:1 E:1', filename='Fallout.S01E01.La.Fine.1080p.AMZN.WEB-DL.ITA.ENG.DDP5.1.H.264.mkv', size='5250906', quality='1080', duration='4498', views=0, is_viewable=1, status='public', fps=24, legacy=0, folder_id=81aff513-a0c4-4723-bacc-fca2d1143eb5, created_at_diff='3 settimane fa')" - - # Initialize WindowVideo object - win_video = WindowVideo(json_video) - - # Assert - self.assertEqual(str(win_video), expected_output) - -class TestWindowParameter(unittest.TestCase): - def test_str(self): - # Test data - json_parameter = {'token': 'cWDn4XKemoNr7PnPghud2Q', 'token360p': '', 'token480p': 'arPfGVKQ7Dk7wh9xJ8QJDA', 'token720p': 'fuEf67Z1WuD-OgWzS7OxxA', 'token1080p': '3O-DrtT7I3ZmecYQgpC45A', 'expires': '1720094851'} - expected_output = "WindowParameter(token='cWDn4XKemoNr7PnPghud2Q', token360p='', token480p='arPfGVKQ7Dk7wh9xJ8QJDA', token720p='fuEf67Z1WuD-OgWzS7OxxA', token1080p='3O-DrtT7I3ZmecYQgpC45A', expires='1720094851')" - - # Initialize WindowParameter object - win_par = WindowParameter(json_parameter) - - # Assert - self.assertEqual(str(win_par), expected_output) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file