diff --git a/.github/workflows/status.yml b/.github/workflows/status.yml index 83e5b9c1..a777f0fb 100644 --- a/.github/workflows/status.yml +++ b/.github/workflows/status.yml @@ -37,7 +37,7 @@ jobs: #- name: TEST Favorites # run: python -m unittest -v test_favorites.TestFavorites.test_programs - name: TEST ResumePoints - run: python -m unittest -v test_resumepoints.TestResumePoints.test_get_continue_episodes + run: python -m unittest -v test_api.TestApi.test_get_continue_episodes if: always() - name: TEST Search run: python -m unittest -v test_search.TestSearch.test_search_journaal diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index a0937204..31788150 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -127,7 +127,7 @@ msgid "Documentaries" msgstr "" msgctxt "#30045" -msgid "All [I]one-off[/I] documentaries on VRT MAX" +msgid "All documentaries on VRT MAX" msgstr "" msgctxt "#30046" @@ -135,7 +135,7 @@ msgid "Music" msgstr "" msgctxt "#30047" -msgid "All [I]one-off[/I] music on VRT MAX" +msgid "All music on VRT MAX" msgstr "" msgctxt "#30048" @@ -512,7 +512,11 @@ msgid "from livestream cache" msgstr "" msgctxt "#30455" -msgid "Delete from this list" +msgid "Delete from this list (VRT MAX)" +msgstr "" + +msgctxt "#30456" +msgid "Mark as watched (VRT MAX)" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index a45de0e4..3c8b04d2 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -127,16 +127,16 @@ msgid "Documentaries" msgstr "Documentaires" msgctxt "#30045" -msgid "All [I]one-off[/I] documentaries on VRT MAX" -msgstr "Alle [I]one-off[/I] documentaires op VRT MAX" +msgid "All documentaries on VRT MAX" +msgstr "Alle documentaires op VRT MAX" msgctxt "#30046" msgid "Music" msgstr "Muziek" msgctxt "#30047" -msgid "All [I]one-off[/I] music on VRT MAX" -msgstr "Alle [I]one-off[/I] muziek op VRT MAX" +msgid "All music on VRT MAX" +msgstr "Alle muziek op VRT MAX" msgctxt "#30048" msgid "Most recent" @@ -512,8 +512,12 @@ msgid "from livestream cache" msgstr "uit de livestream-cache" msgctxt "#30455" -msgid "Delete from this list" -msgstr "Verwijder uit deze lijst" +msgid "Delete from this list (VRT MAX)" +msgstr "Verwijder uit deze lijst (VRT MAX)" + +msgctxt "#30456" +msgid "Mark as watched (VRT MAX)" +msgstr "Markeren als afgespeeld (VRT MAX)" ### SETTINGS diff --git a/resources/lib/addon.py b/resources/lib/addon.py index 2dcf778f..20332a5c 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -75,20 +75,6 @@ def favorites_programs(end_cursor=''): VRTPlayer().show_tvshow_menu(end_cursor=end_cursor, use_favorites=True) -@plugin.route('/favorites/docu') -def favorites_docu(): - """The favorites docu listing""" - from vrtplayer import VRTPlayer - VRTPlayer().show_favorites_docu_menu() - - -@plugin.route('/favorites/music') -def favorites_music(): - """The favorites music listing""" - from vrtplayer import VRTPlayer - VRTPlayer().show_favorites_music_menu() - - @plugin.route('/favorites/recent') @plugin.route('/favorites/recent') @plugin.route('/favorites/recent/') @@ -132,16 +118,15 @@ def resumepoints_continue(end_cursor=''): @plugin.route('/resumepoints/continue/delete/') def resumepoints_continue_delete(episode_id): """The API interface to delete episodes from continue watching listing""" - from resumepoints import ResumePoints - ResumePoints().delete_continue(episode_id) + from api import delete_continue + delete_continue(episode_id) -@plugin.route('/resumepoints/refresh') -def resumepoints_refresh(): - """The API interface to refresh the resumepoints cache""" - from resumepoints import ResumePoints - ResumePoints().refresh(ttl=0) - notification(message=localize(30983)) +@plugin.route('/resumepoints/continue/finish/') +def resumepoints_continue_finish(episode_id): + """The API interface to finish episodes from continue watching listing""" + from api import finish_continue + finish_continue(episode_id) @plugin.route('/programs/') diff --git a/resources/lib/api.py b/resources/lib/api.py index 227690c8..cc9ae6c4 100644 --- a/resources/lib/api.py +++ b/resources/lib/api.py @@ -76,6 +76,10 @@ def get_context_menu(program_name, program_id, program_title, program_type, is_f localize(30455), # Delete from this list 'RunPlugin(%s)' % url_for('resumepoints_continue_delete', episode_id=episode_id) )) + context_menu.append(( + localize(30456), # Mark as watched (VRT MAX) + 'RunPlugin(%s)' % url_for('resumepoints_continue_finish', episode_id=episode_id) + )) return context_menu @@ -561,6 +565,7 @@ def get_seasons_data(program_name): def set_resumepoint(video_id, title, position, total): """Set resumepoint""" + data_json = {} # Respect resumepoint margins if position and total: if position < RESUMEPOINTS_MARGIN: @@ -585,6 +590,76 @@ def set_resumepoint(video_id, title, position, total): data = dumps(payload).encode('utf-8') data_json = get_url_json(url='{}/{}'.format(RESUMEPOINTS_URL, video_id), cache=None, headers=headers, data=data, raise_errors='all') log(3, '[Resumepoints] Updated resumepoint {data}', data=data_json) + return data_json + + +def delete_continue(episode_id): + """Delete continue episode using GraphQL API""" + import base64 + from json import dumps + graphql_query = """ + mutation listDelete($input: ListDeleteActionInput!) { + setListDeleteActionItem(input: $input) { + title + active + action { + __typename + ... on NoAction { + __typename + reason + } + ... on ListTileDeletedAction { + __typename + listId + listName + id + } + } + __typename + } + } + """ + list_name = { + 'listId': 'dynamic:/vrtnu.model.json@resume-list-video', + 'listType': 'verderkijken', + } + encoded_list_name = base64.b64encode(dumps(list_name).encode('utf-8')) + operation_name = 'listDelete' + variables = { + 'input': { + 'id': episode_id, + 'listName': encoded_list_name.decode('utf-8'), + }, + } + return api_req(graphql_query, operation_name, variables) + + +def finish_continue(episode_id): + """Finish continue episode using GraphQL API""" + graphql_query = """ + mutation finishItem($input: FinishActionInput!) { + setFinishActionItem(input: $input) { + __typename + objectId + title + accessibilityLabel + action { + ... on FinishAction { + id + __typename + } + __typename + } + } + } + """ + operation_name = 'finishItem' + variables = { + 'input': { + 'id': episode_id, + }, + } + return api_req(graphql_query, operation_name, variables) def get_paginated_episodes(list_id, page_size, end_cursor=''): @@ -1256,7 +1331,7 @@ def api_req(graphql_query, operation_name, variables, client='WEB'): 'Authorization': 'Bearer ' + access_token, 'Content-Type': 'application/json', 'x-vrt-client-name': client, - 'x-vrt-client-version': '1.5.0', + 'x-vrt-client-version': '1.5.7', } data_json = get_url_json(url=GRAPHQL_URL, cache=None, headers=headers, data=data, raise_errors='all') return data_json diff --git a/resources/lib/resumepoints.py b/resources/lib/resumepoints.py index a6267141..cad9c210 100644 --- a/resources/lib/resumepoints.py +++ b/resources/lib/resumepoints.py @@ -174,6 +174,11 @@ def delete_continue(self, episode_id): self._delete_continue_graphql(episode_id) container_refresh() + def finish_continue(self, episode_id): + """Finish a continue item from continue menu""" + self._finish_continue_graphql(episode_id) + container_refresh() + def _delete_continue_graphql(self, episode_id): """Delete continue episode using GraphQL API""" from tokenresolver import TokenResolver @@ -229,6 +234,49 @@ def _delete_continue_graphql(self, episode_id): result_json = get_url_json(url=self.GRAPHQL_URL, cache=None, headers=headers, data=data, raise_errors='all') return result_json + def _finish_continue_graphql(self, episode_id): + """Finish continue episode using GraphQL API""" + from tokenresolver import TokenResolver + from json import dumps + access_token = TokenResolver().get_token('vrtnu-site_profile_at') + result_json = {} + if access_token: + headers = { + 'Authorization': 'Bearer ' + access_token, + 'Content-Type': 'application/json', + 'x-vrt-client-name': 'WEB', + 'x-vrt-client-version': '1.5.0', + } + graphql_query = """ + mutation finishItem($input: FinishActionInput!) { + setFinishActionItem(input: $input) { + __typename + objectId + title + accessibilityLabel + action { + ... on FinishAction { + id + __typename + } + __typename + } + } + } + """ + payload = { + 'operationName': 'finishItem', + 'variables': { + 'input': { + 'id': episode_id, + }, + }, + 'query': graphql_query, + } + data = dumps(payload).encode('utf-8') + result_json = get_url_json(url=self.GRAPHQL_URL, cache=None, headers=headers, data=data, raise_errors='all') + return result_json + def get_continue(self): """Get continue using GraphQL API""" from tokenresolver import TokenResolver diff --git a/resources/lib/vrtplayer.py b/resources/lib/vrtplayer.py index addbc849..2e618572 100644 --- a/resources/lib/vrtplayer.py +++ b/resources/lib/vrtplayer.py @@ -156,7 +156,7 @@ def show_favorites_menu(self): if get_setting_bool('addmydocu', default=True): favorites_items.append( TitleItem(label=localize(30044), # My documentaries - path=url_for('favorites_docu'), + path=url_for('categories', category='docu'), art_dict={'thumb': 'DefaultMovies.png'}, info_dict={'plot': localize(30045)}) ) @@ -164,7 +164,7 @@ def show_favorites_menu(self): if get_setting_bool('addmymusic', default=True): favorites_items.append( TitleItem(label=localize(30046), # My music - path=url_for('favorites_music'), + path=url_for('categories', category='muziek'), art_dict={'thumb': 'DefaultAddonMusic.png'}, info_dict={'plot': localize(30047)}) ) @@ -270,10 +270,6 @@ def show_offline_menu(self, end_cursor='', use_favorites=False): def show_continue_menu(self, end_cursor=''): """The VRT MAX add-on 'Continue waching' listing menu""" - - # Continue watching menu may need more up-to-date favorites - self._favorites.refresh(ttl=ttl('direct')) - self._resumepoints.refresh(ttl=ttl('direct')) episodes, sort, ascending, content = get_continue_episodes(end_cursor=end_cursor) show_listing(episodes, category=30054, sort=sort, ascending=ascending, content=content, cache=False) diff --git a/tests/test_api.py b/tests/test_api.py index c5b5cc55..8d0f57db 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,8 +6,9 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from api import (get_episodes, get_favorite_programs, get_latest_episode, get_next_info, get_online_categories, - get_offline_programs, get_programs, get_recent_episodes, get_search, get_single_episode, valid_categories) +from api import (delete_continue, finish_continue, get_continue_episodes, get_episodes, get_favorite_programs, get_latest_episode, + get_next_info, get_online_categories, get_offline_programs, get_programs, get_recent_episodes, + get_resumepoint_data, get_search, get_single_episode, set_resumepoint, valid_categories) from data import CATEGORIES from xbmcextra import kodi_to_ansi @@ -123,6 +124,50 @@ def test_upnext(self): self.assertTrue(next_episode) print(next_episode) + def test_set_resumepoint(self): + """Test setting resumepoint for episode wij--roger-raveel""" + video_id = 'vid-b643dbd8-03d1-4ceb-a738-262d6fa7c271' + position = 1337.242753 + total = 3334.04 + title = 'Wij, Roger Raveel' + response = set_resumepoint(video_id, title, position, total) + self.assertEqual(response.get('at'), int(position)) + + def test_get_resumepoint(self): + """Test getting resumepoint for episode wij--roger-raveel""" + video_id = 'vid-b643dbd8-03d1-4ceb-a738-262d6fa7c271' + episode_id = '1615881736655' + media_id, _ = get_resumepoint_data(episode_id) + self.assertEqual(media_id, video_id) + + def test_get_continue_episodes(self): + """Test getting continue watching list""" + + # Ensure a continue episode exists (Wij, Roger Raveel) + video_id = 'vid-b643dbd8-03d1-4ceb-a738-262d6fa7c271' + position = 1337.242753 + total = 3334.04 + title = 'Wij, Roger Raveel' + set_resumepoint(video_id, title, position, total) + + episode_items, sort, ascending, content = get_continue_episodes() + self.assertTrue(episode_items) + self.assertEqual(sort, 'dateadded') + self.assertFalse(ascending) + self.assertEqual(content, 'episodes') + + def test_finish_continue(self): + """Test finish continue watching for episode wij--roger-raveel""" + episode_id = '1615881736655' + data = finish_continue(episode_id) + self.assertEqual(data.get('data').get('setFinishActionItem').get('title'), 'Afgespeeld') + + def test_delete_continue(self): + """Test delete continue watching for episode wij--roger-raveel""" + episode_id = '1615881736655' + data = delete_continue(episode_id) + self.assertEqual(data.get('data').get('setListDeleteActionItem').get('title'), 'Verwijderd') + def test_get_categories(self): """Test to ensure our local hardcoded categories conforms to online categories""" # Remove thumbnails from scraped categories first diff --git a/tests/test_resumepoints.py b/tests/test_resumepoints.py deleted file mode 100644 index fa9c7ffa..00000000 --- a/tests/test_resumepoints.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: (c) 2019, Dag Wieers (@dagwieers) -# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -"""Unit tests for ResumePoints functionality""" - -# pylint: disable=invalid-name - -from __future__ import absolute_import, division, print_function, unicode_literals -import unittest -from api import get_continue_episodes -from resumepoints import ResumePoints - -xbmc = __import__('xbmc') -xbmcaddon = __import__('xbmcaddon') -xbmcgui = __import__('xbmcgui') -xbmcplugin = __import__('xbmcplugin') -xbmcvfs = __import__('xbmcvfs') - -addon = xbmcaddon.Addon() -addon.settings['useresumepoints'] = True - - -class TestResumePoints(unittest.TestCase): - """TestCase class""" - - _resumepoints = ResumePoints() - - @unittest.skipUnless(addon.settings.get('username'), 'Skipping as VRT username is missing.') - @unittest.skipUnless(addon.settings.get('password'), 'Skipping as VRT password is missing.') - def test_get_continue_episodes(self): - """Test items, sort and order""" - - # Ensure a continue episode exists (Winteruur met Lize Feryn (beschikbaar tot 26 maart 2025)) - self._resumepoints.update_resumepoint( - video_id='vid-271d7238-b7f2-4a3c-b3c7-17a5110be71a', - asset_str='winteruur - 5 - lize feryn', - title='Winteruur', - position=38, - total=635, - ) - - episode_items, sort, ascending, content = get_continue_episodes() - self.assertTrue(episode_items) - self.assertEqual(sort, 'dateadded') - self.assertFalse(ascending) - self.assertEqual(content, 'episodes') - - def test_update_none(self): - """Test updating empty resumepoints""" - self.assertTrue(self._resumepoints.update_resumepoint(video_id=None, asset_str=None, title=None)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_routing.py b/tests/test_routing.py index d09cbdbd..03be6834 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -209,11 +209,6 @@ def test_refresh_favorites_route(self): addon.run(['plugin://plugin.video.vrt.nu/favorites/refresh', '0', '']) self.assertEqual(plugin.url_for(addon.favorites_refresh), 'plugin://plugin.video.vrt.nu/favorites/refresh') - def test_refresh_resumepoints_route(self): - """Refresh resumepoints method: /resumepoints/refresh""" - addon.run(['plugin://plugin.video.vrt.nu/resumepoints/refresh', '0', '']) - self.assertEqual(plugin.url_for(addon.resumepoints_refresh), 'plugin://plugin.video.vrt.nu/resumepoints/refresh') - def test_manage_favorites_route(self): """Manage favorites method: /favorites/manage""" addon.run(['plugin://plugin.video.vrt.nu/favorites/manage', '0', ''])